Remove obsolete Email v1 implementation (#1365)

This commit is contained in:
Aditya Chandel
2025-10-16 13:20:11 -06:00
committed by GitHub
parent 257a2efeb3
commit 8d0d23ba85
58 changed files with 232 additions and 1903 deletions

View File

@@ -1,32 +0,0 @@
package com.adityachandel.booklore.controller;
import com.adityachandel.booklore.model.dto.request.SendBookByEmailRequest;
import com.adityachandel.booklore.service.email.EmailService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Deprecated
@AllArgsConstructor
@RestController
@RequestMapping("/api/v1/emails")
public class EmailController {
private final EmailService emailService;
@PreAuthorize("@securityUtil.canEmailBook() or @securityUtil.isAdmin()")
@PostMapping("/send-book")
public ResponseEntity<?> sendEmail(@Validated @RequestBody SendBookByEmailRequest request) {
emailService.emailBook(request);
return ResponseEntity.noContent().build();
}
@PreAuthorize("@securityUtil.canEmailBook() or @securityUtil.isAdmin()")
@PostMapping("/send-book/{bookId}")
public ResponseEntity<?> emailBookQuick(@PathVariable Long bookId) {
emailService.emailBookQuick(bookId);
return ResponseEntity.noContent().build();
}
}

View File

@@ -1,58 +0,0 @@
package com.adityachandel.booklore.controller;
import com.adityachandel.booklore.model.dto.EmailProvider;
import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest;
import com.adityachandel.booklore.service.email.EmailProviderService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Deprecated
@AllArgsConstructor
@RestController
@RequestMapping("/api/v1/email/providers")
public class EmailProviderController {
private final EmailProviderService emailProviderService;
@PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canEmailBook()")
@GetMapping
public ResponseEntity<List<EmailProvider>> getEmailProviders() {
return ResponseEntity.ok(emailProviderService.getEmailProviders());
}
@PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canEmailBook()")
@GetMapping("/{id}")
public ResponseEntity<EmailProvider> getEmailProvider(@PathVariable Long id) {
return ResponseEntity.ok(emailProviderService.getEmailProvider(id));
}
@PreAuthorize("@securityUtil.isAdmin()")
@PostMapping
public ResponseEntity<EmailProvider> createEmailProvider(@RequestBody CreateEmailProviderRequest createEmailProviderRequest) {
return ResponseEntity.ok(emailProviderService.createEmailProvider(createEmailProviderRequest));
}
@PreAuthorize("@securityUtil.isAdmin()")
@PutMapping("/{id}")
public ResponseEntity<EmailProvider> updateEmailProvider(@PathVariable Long id, @RequestBody CreateEmailProviderRequest updateRequest) {
return ResponseEntity.ok(emailProviderService.updateEmailProvider(id, updateRequest));
}
@PreAuthorize("@securityUtil.isAdmin()")
@PatchMapping("/{id}/set-default")
public ResponseEntity<Void> setDefaultEmailProvider(@PathVariable Long id) {
emailProviderService.setDefaultEmailProvider(id);
return ResponseEntity.noContent().build();
}
@PreAuthorize("@securityUtil.isAdmin()")
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteEmailProvider(@PathVariable Long id) {
emailProviderService.deleteEmailProvider(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -1,58 +0,0 @@
package com.adityachandel.booklore.controller;
import com.adityachandel.booklore.model.dto.EmailRecipient;
import com.adityachandel.booklore.model.dto.request.CreateEmailRecipientRequest;
import com.adityachandel.booklore.service.email.EmailRecipientService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Deprecated
@AllArgsConstructor
@RestController
@RequestMapping("/api/v1/email/recipients")
public class EmailRecipientController {
private final EmailRecipientService emailRecipientService;
@PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canEmailBook()")
@GetMapping
public ResponseEntity<List<EmailRecipient>> getEmailRecipients() {
return ResponseEntity.ok(emailRecipientService.getEmailRecipients());
}
@PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canEmailBook()")
@GetMapping("/{id}")
public ResponseEntity<EmailRecipient> getEmailRecipient(@PathVariable Long id) {
return ResponseEntity.ok(emailRecipientService.getEmailRecipient(id));
}
@PreAuthorize("@securityUtil.isAdmin()")
@PostMapping
public ResponseEntity<EmailRecipient> createEmailRecipient(@RequestBody CreateEmailRecipientRequest createEmailRecipientRequest) {
return ResponseEntity.ok(emailRecipientService.createEmailRecipient(createEmailRecipientRequest));
}
@PreAuthorize("@securityUtil.isAdmin()")
@PutMapping("/{id}")
public ResponseEntity<EmailRecipient> updateEmailRecipient(@PathVariable Long id, @RequestBody CreateEmailRecipientRequest updateRequest) {
return ResponseEntity.ok(emailRecipientService.updateEmailRecipient(id, updateRequest));
}
@PreAuthorize("@securityUtil.isAdmin()")
@PatchMapping("/{id}/set-default")
public ResponseEntity<Void> setDefaultEmailRecipient(@PathVariable Long id) {
emailRecipientService.setDefaultRecipient(id);
return ResponseEntity.noContent().build();
}
@PreAuthorize("@securityUtil.isAdmin()")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteEmailRecipient(@PathVariable Long id) {
emailRecipientService.deleteEmailRecipient(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -1,7 +1,6 @@
package com.adityachandel.booklore.controller;
import com.adityachandel.booklore.model.dto.request.SendBookByEmailRequest;
import com.adityachandel.booklore.service.email.EmailService;
import com.adityachandel.booklore.service.email.SendEmailV2Service;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;

View File

@@ -1,16 +0,0 @@
package com.adityachandel.booklore.mapper;
import com.adityachandel.booklore.model.dto.EmailProvider;
import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest;
import com.adityachandel.booklore.model.entity.EmailProviderEntity;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
@Mapper(componentModel = "spring")
public interface EmailProviderMapper {
EmailProvider toDTO(EmailProviderEntity emailProviderEntity);
EmailProviderEntity toEntity(EmailProvider emailProvider);
EmailProviderEntity toEntity(CreateEmailProviderRequest createRequest);
void updateEntityFromRequest(CreateEmailProviderRequest request, @MappingTarget EmailProviderEntity entity);
}

View File

@@ -3,14 +3,24 @@ package com.adityachandel.booklore.mapper;
import com.adityachandel.booklore.model.dto.EmailProviderV2;
import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest;
import com.adityachandel.booklore.model.entity.EmailProviderV2Entity;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.*;
@Mapper(componentModel = "spring")
public interface EmailProviderV2Mapper {
EmailProviderV2 toDTO(EmailProviderV2Entity entity);
EmailProviderV2Entity toEntity(EmailProviderV2 emailProvider);
EmailProviderV2Entity toEntity(CreateEmailProviderRequest createRequest);
@Mapping(target = "defaultProvider", expression = "java(entity.getId() != null && entity.getId().equals(defaultProviderId))")
EmailProviderV2 toDTO(EmailProviderV2Entity entity, @Context Long defaultProviderId);
@Mapping(target = "id", ignore = true)
@Mapping(target = "userId", ignore = true)
@Mapping(target = "defaultProvider", ignore = true)
@Mapping(target = "shared", ignore = true)
EmailProviderV2Entity toEntity(CreateEmailProviderRequest request);
@Mapping(target = "id", ignore = true)
@Mapping(target = "userId", ignore = true)
@Mapping(target = "defaultProvider", ignore = true)
@Mapping(target = "shared", ignore = true)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntityFromRequest(CreateEmailProviderRequest request, @MappingTarget EmailProviderV2Entity entity);
}

View File

@@ -1,19 +0,0 @@
package com.adityachandel.booklore.mapper;
import com.adityachandel.booklore.model.dto.EmailRecipient;
import com.adityachandel.booklore.model.dto.request.CreateEmailRecipientRequest;
import com.adityachandel.booklore.model.entity.EmailRecipientEntity;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
@Mapper(componentModel = "spring")
public interface EmailRecipientMapper {
EmailRecipient toDTO(EmailRecipientEntity emailRecipientEntity);
EmailRecipientEntity toEntity(EmailRecipient emailRecipient);
EmailRecipientEntity toEntity(CreateEmailRecipientRequest createRequest);
void updateEntityFromRequest(CreateEmailRecipientRequest request, @MappingTarget EmailRecipientEntity entity);
}

View File

@@ -1,24 +0,0 @@
package com.adityachandel.booklore.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Deprecated
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmailProvider {
private Long id;
private String name;
private String host;
private Integer port;
private String username;
private String password;
private String fromAddress;
private Boolean auth;
private Boolean startTls;
private Boolean defaultProvider;
}

View File

@@ -16,7 +16,6 @@ public class EmailProviderV2 {
private String host;
private Integer port;
private String username;
private String password;
private String fromAddress;
private Boolean auth;
private Boolean startTls;

View File

@@ -1,18 +0,0 @@
package com.adityachandel.booklore.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Deprecated
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmailRecipient {
private Long id;
private String email;
private String name;
private boolean defaultRecipient;
}

View File

@@ -1,46 +0,0 @@
package com.adityachandel.booklore.model.entity;
import jakarta.persistence.*;
import lombok.*;
@Deprecated
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "email_provider")
public class EmailProviderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
@Column(name = "host", nullable = false)
private String host;
@Column(name = "port", nullable = false)
private int port;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "from_address")
private String fromAddress;
@Column(name = "auth", nullable = false)
private boolean auth;
@Column(name = "start_tls", nullable = false)
private boolean startTls;
@Column(name = "is_default", nullable = false)
private boolean defaultProvider;
}

View File

@@ -1,28 +0,0 @@
package com.adityachandel.booklore.model.entity;
import jakarta.persistence.*;
import lombok.*;
@Deprecated
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "email_recipient")
public class EmailRecipientEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "is_default", nullable = false)
private boolean defaultRecipient;
}

View File

@@ -0,0 +1,27 @@
package com.adityachandel.booklore.model.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user_email_provider_preference", uniqueConstraints = {
@UniqueConstraint(columnNames = {"user_id"})
})
public class UserEmailProviderPreferenceEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "default_provider_id", nullable = false)
private Long defaultProviderId;
}

View File

@@ -1,21 +0,0 @@
package com.adityachandel.booklore.repository;
import com.adityachandel.booklore.model.entity.EmailProviderEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Deprecated
@Repository
public interface EmailProviderRepository extends JpaRepository<EmailProviderEntity, Long> {
@Modifying
@Query("UPDATE EmailProviderEntity e SET e.defaultProvider = false")
void updateAllProvidersToNonDefault();
@Query("SELECT e FROM EmailProviderEntity e WHERE e.defaultProvider = true")
Optional<EmailProviderEntity> findDefaultEmailProvider();
}

View File

@@ -2,7 +2,6 @@ package com.adityachandel.booklore.repository;
import com.adityachandel.booklore.model.entity.EmailProviderV2Entity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@@ -17,16 +16,12 @@ public interface EmailProviderV2Repository extends JpaRepository<EmailProviderV2
List<EmailProviderV2Entity> findAllByUserId(Long userId);
@Modifying
@Query("UPDATE EmailProviderV2Entity e SET e.defaultProvider = false")
void updateAllProvidersToNonDefault();
@Query("SELECT e FROM EmailProviderV2Entity e WHERE e.userId = :userId AND e.defaultProvider = true")
Optional<EmailProviderV2Entity> findDefaultEmailProvider(@Param("userId") Long userId);
@Query("SELECT e FROM EmailProviderV2Entity e WHERE e.shared = true AND e.userId IN (SELECT u.id FROM BookLoreUserEntity u WHERE u.permissions.permissionAdmin = true)")
List<EmailProviderV2Entity> findAllBySharedTrueAndAdmin();
@Query("SELECT e FROM EmailProviderV2Entity e WHERE e.id = :id AND e.shared = true AND e.userId IN (SELECT u.id FROM BookLoreUserEntity u WHERE u.permissions.permissionAdmin = true)")
Optional<EmailProviderV2Entity> findSharedProviderById(@Param("id") Long id);
@Query("SELECT e FROM EmailProviderV2Entity e WHERE e.id = :id AND (e.userId = :userId OR (e.shared = true AND e.userId IN (SELECT u.id FROM BookLoreUserEntity u WHERE u.permissions.permissionAdmin = true)))")
Optional<EmailProviderV2Entity> findAccessibleProvider(@Param("id") Long id, @Param("userId") Long userId);
}

View File

@@ -1,25 +0,0 @@
package com.adityachandel.booklore.repository;
import com.adityachandel.booklore.model.entity.EmailRecipientEntity;
import jakarta.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Deprecated
@Repository
public interface EmailRecipientRepository extends JpaRepository<EmailRecipientEntity, Long> {
Optional<EmailRecipientEntity> findById(long id);
@Modifying
@Transactional
@Query("UPDATE EmailRecipientEntity e SET e.defaultRecipient = false WHERE e.defaultRecipient = true")
void updateAllRecipientsToNonDefault();
@Query("SELECT e FROM EmailRecipientEntity e WHERE e.defaultRecipient = true")
Optional<EmailRecipientEntity> findDefaultEmailRecipient();
}

View File

@@ -1,6 +1,5 @@
package com.adityachandel.booklore.repository;
import com.adityachandel.booklore.model.entity.EmailRecipientEntity;
import com.adityachandel.booklore.model.entity.EmailRecipientV2Entity;
import jakarta.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -20,9 +19,9 @@ public interface EmailRecipientV2Repository extends JpaRepository<EmailRecipient
@Modifying
@Transactional
@Query("UPDATE EmailRecipientV2Entity e SET e.defaultRecipient = false WHERE e.defaultRecipient = true")
void updateAllRecipientsToNonDefault();
@Query("UPDATE EmailRecipientV2Entity e SET e.defaultRecipient = false WHERE e.defaultRecipient = true AND e.userId = :userId")
void updateAllRecipientsToNonDefault(Long userId);
@Query("SELECT e FROM EmailRecipientV2Entity e WHERE e.defaultRecipient = true")
Optional<EmailRecipientV2Entity> findDefaultEmailRecipient();
@Query("SELECT e FROM EmailRecipientV2Entity e WHERE e.defaultRecipient = true AND e.userId = :userId")
Optional<EmailRecipientV2Entity> findDefaultEmailRecipientByUserId(Long userId);
}

View File

@@ -0,0 +1,16 @@
package com.adityachandel.booklore.repository;
import com.adityachandel.booklore.model.entity.UserEmailProviderPreferenceEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserEmailProviderPreferenceRepository extends JpaRepository<UserEmailProviderPreferenceEntity, Long> {
Optional<UserEmailProviderPreferenceEntity> findByUserId(Long userId);
void deleteByUserId(Long userId);
}

View File

@@ -1,73 +0,0 @@
package com.adityachandel.booklore.service.email;
import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.mapper.EmailProviderMapper;
import com.adityachandel.booklore.model.dto.EmailProvider;
import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest;
import com.adityachandel.booklore.model.entity.EmailProviderEntity;
import com.adityachandel.booklore.repository.EmailProviderRepository;
import jakarta.transaction.Transactional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@Deprecated
@Slf4j
@Service
@AllArgsConstructor
public class EmailProviderService {
private final EmailProviderRepository emailProviderRepository;
private final EmailProviderMapper emailProviderMapper;
public EmailProvider getEmailProvider(Long id) {
EmailProviderEntity emailProvider = emailProviderRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
return emailProviderMapper.toDTO(emailProvider);
}
public EmailProvider createEmailProvider(CreateEmailProviderRequest request) {
boolean isFirstProvider = emailProviderRepository.count() == 0;
EmailProviderEntity emailProviderEntity = emailProviderMapper.toEntity(request);
emailProviderEntity.setDefaultProvider(isFirstProvider);
EmailProviderEntity savedEntity = emailProviderRepository.save(emailProviderEntity);
return emailProviderMapper.toDTO(savedEntity);
}
public EmailProvider updateEmailProvider(Long id, CreateEmailProviderRequest request) {
EmailProviderEntity existingProvider = emailProviderRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
emailProviderMapper.updateEntityFromRequest(request, existingProvider);
EmailProviderEntity updatedEntity = emailProviderRepository.save(existingProvider);
return emailProviderMapper.toDTO(updatedEntity);
}
@Transactional
public void setDefaultEmailProvider(Long id) {
EmailProviderEntity emailProvider = emailProviderRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
emailProviderRepository.updateAllProvidersToNonDefault();
emailProvider.setDefaultProvider(true);
emailProviderRepository.save(emailProvider);
}
@Transactional
public void deleteEmailProvider(Long id) {
EmailProviderEntity emailProviderToDelete = emailProviderRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
boolean isDefaultProvider = emailProviderToDelete.isDefaultProvider();
if (isDefaultProvider) {
List<EmailProviderEntity> allProviders = emailProviderRepository.findAll();
if (allProviders.size() > 1) {
allProviders.remove(emailProviderToDelete);
EmailProviderEntity newDefaultProvider = allProviders.get(ThreadLocalRandom.current().nextInt(allProviders.size()));
newDefaultProvider.setDefaultProvider(true);
emailProviderRepository.save(newDefaultProvider);
}
}
emailProviderRepository.deleteById(id);
}
public List<EmailProvider> getEmailProviders() {
return emailProviderRepository.findAll().stream().map(emailProviderMapper::toDTO).toList();
}
}

View File

@@ -7,7 +7,9 @@ import com.adityachandel.booklore.model.dto.BookLoreUser;
import com.adityachandel.booklore.model.dto.EmailProviderV2;
import com.adityachandel.booklore.model.dto.request.CreateEmailProviderRequest;
import com.adityachandel.booklore.model.entity.EmailProviderV2Entity;
import com.adityachandel.booklore.model.entity.UserEmailProviderPreferenceEntity;
import com.adityachandel.booklore.repository.EmailProviderV2Repository;
import com.adityachandel.booklore.repository.UserEmailProviderPreferenceRepository;
import jakarta.transaction.Transactional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -22,71 +24,120 @@ import java.util.concurrent.ThreadLocalRandom;
public class EmailProviderV2Service {
private final EmailProviderV2Repository repository;
private final UserEmailProviderPreferenceRepository preferenceRepository;
private final EmailProviderV2Mapper mapper;
private final AuthenticationService authService;
public List<EmailProviderV2> getEmailProviders() {
BookLoreUser user = authService.getAuthenticatedUser();
List<EmailProviderV2Entity> userProviders = repository.findAllByUserId(user.getId());
if (user.getPermissions().isAdmin()) {
return userProviders.stream().map(mapper::toDTO).toList();
if (!user.getPermissions().isAdmin()) {
List<EmailProviderV2Entity> sharedProviders = repository.findAllBySharedTrueAndAdmin();
userProviders.addAll(sharedProviders);
}
List<EmailProviderV2Entity> sharedProviders = repository.findAllBySharedTrueAndAdmin();
userProviders.addAll(sharedProviders);
return userProviders.stream().map(mapper::toDTO).toList();
Long defaultProviderId = getDefaultProviderIdForUser(user.getId());
return userProviders.stream()
.map(entity -> mapper.toDTO(entity, defaultProviderId))
.toList();
}
public EmailProviderV2 getEmailProvider(Long id) {
BookLoreUser user = authService.getAuthenticatedUser();
EmailProviderV2Entity entity = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
return mapper.toDTO(entity);
EmailProviderV2Entity entity = repository.findAccessibleProvider(id, user.getId())
.orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
Long defaultProviderId = getDefaultProviderIdForUser(user.getId());
return mapper.toDTO(entity, defaultProviderId);
}
@Transactional
public EmailProviderV2 createEmailProvider(CreateEmailProviderRequest request) {
BookLoreUser user = authService.getAuthenticatedUser();
boolean isFirstProvider = repository.count() == 0;
EmailProviderV2Entity entity = mapper.toEntity(request);
entity.setDefaultProvider(isFirstProvider);
entity.setUserId(user.getId());
entity.setShared(user.getPermissions().isAdmin() && request.isShared());
EmailProviderV2Entity savedEntity = repository.save(entity);
return mapper.toDTO(savedEntity);
if (preferenceRepository.findByUserId(user.getId()).isEmpty()) {
setDefaultProviderForUser(user.getId(), savedEntity.getId());
}
Long defaultProviderId = getDefaultProviderIdForUser(user.getId());
return mapper.toDTO(savedEntity, defaultProviderId);
}
@Transactional
public EmailProviderV2 updateEmailProvider(Long id, CreateEmailProviderRequest request) {
BookLoreUser user = authService.getAuthenticatedUser();
EmailProviderV2Entity existingProvider = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
EmailProviderV2Entity existingProvider = repository.findByIdAndUserId(id, user.getId())
.orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
mapper.updateEntityFromRequest(request, existingProvider);
if (user.getPermissions().isAdmin()) {
existingProvider.setShared(request.isShared());
}
EmailProviderV2Entity updatedEntity = repository.save(existingProvider);
return mapper.toDTO(updatedEntity);
Long defaultProviderId = getDefaultProviderIdForUser(user.getId());
return mapper.toDTO(updatedEntity, defaultProviderId);
}
@Transactional
public void setDefaultEmailProvider(Long id) {
BookLoreUser user = authService.getAuthenticatedUser();
EmailProviderV2Entity emailProvider = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
repository.updateAllProvidersToNonDefault();
emailProvider.setDefaultProvider(true);
repository.save(emailProvider);
// Verify user has access to this provider
repository.findAccessibleProvider(id, user.getId())
.orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
setDefaultProviderForUser(user.getId(), id);
}
@Transactional
public void deleteEmailProvider(Long id) {
BookLoreUser user = authService.getAuthenticatedUser();
EmailProviderV2Entity emailProviderToDelete = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
boolean isDefaultProvider = emailProviderToDelete.isDefaultProvider();
if (isDefaultProvider) {
List<EmailProviderV2Entity> allProviders = repository.findAll();
if (allProviders.size() > 1) {
allProviders.remove(emailProviderToDelete);
EmailProviderV2Entity newDefaultProvider = allProviders.get(ThreadLocalRandom.current().nextInt(allProviders.size()));
newDefaultProvider.setDefaultProvider(true);
repository.save(newDefaultProvider);
repository.findByIdAndUserId(id, user.getId())
.orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(id));
List<UserEmailProviderPreferenceEntity> preferencesUsingProvider =
preferenceRepository.findAll().stream()
.filter(pref -> pref.getDefaultProviderId().equals(id))
.toList();
for (UserEmailProviderPreferenceEntity preference : preferencesUsingProvider) {
List<EmailProviderV2Entity> availableProviders = getAccessibleProvidersForUser(preference.getUserId());
availableProviders.removeIf(p -> p.getId().equals(id));
if (!availableProviders.isEmpty()) {
EmailProviderV2Entity newDefault = availableProviders.get(ThreadLocalRandom.current().nextInt(availableProviders.size()));
preference.setDefaultProviderId(newDefault.getId());
preferenceRepository.save(preference);
} else {
preferenceRepository.delete(preference);
}
}
repository.deleteById(id);
}
private Long getDefaultProviderIdForUser(Long userId) {
return preferenceRepository.findByUserId(userId)
.map(UserEmailProviderPreferenceEntity::getDefaultProviderId)
.orElse(null);
}
private void setDefaultProviderForUser(Long userId, Long providerId) {
UserEmailProviderPreferenceEntity preference = preferenceRepository.findByUserId(userId)
.orElse(UserEmailProviderPreferenceEntity.builder()
.userId(userId)
.build());
preference.setDefaultProviderId(providerId);
preferenceRepository.save(preference);
}
private List<EmailProviderV2Entity> getAccessibleProvidersForUser(Long userId) {
List<EmailProviderV2Entity> providers = repository.findAllByUserId(userId);
providers.addAll(repository.findAllBySharedTrueAndAdmin());
return providers;
}
}

View File

@@ -1,84 +0,0 @@
package com.adityachandel.booklore.service.email;
import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.mapper.EmailRecipientMapper;
import com.adityachandel.booklore.model.dto.EmailRecipient;
import com.adityachandel.booklore.model.dto.request.CreateEmailRecipientRequest;
import com.adityachandel.booklore.model.entity.EmailRecipientEntity;
import com.adityachandel.booklore.repository.EmailRecipientRepository;
import jakarta.transaction.Transactional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@Deprecated
@Slf4j
@Service
@AllArgsConstructor
public class EmailRecipientService {
private final EmailRecipientRepository emailRecipientRepository;
private final EmailRecipientMapper emailRecipientMapper;
public EmailRecipient getEmailRecipient(Long id) {
EmailRecipientEntity emailRecipient = emailRecipientRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id));
return emailRecipientMapper.toDTO(emailRecipient);
}
@Transactional
public EmailRecipient createEmailRecipient(CreateEmailRecipientRequest request) {
boolean isFirstRecipient = emailRecipientRepository.count() == 0;
if (request.isDefaultRecipient() || isFirstRecipient) {
emailRecipientRepository.updateAllRecipientsToNonDefault();
}
EmailRecipientEntity emailRecipientEntity = emailRecipientMapper.toEntity(request);
emailRecipientEntity.setDefaultRecipient(request.isDefaultRecipient() || isFirstRecipient);
EmailRecipientEntity savedEntity = emailRecipientRepository.save(emailRecipientEntity);
return emailRecipientMapper.toDTO(savedEntity);
}
@Transactional
public EmailRecipient updateEmailRecipient(Long id, CreateEmailRecipientRequest request) {
EmailRecipientEntity existingRecipient = emailRecipientRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id));
if (request.isDefaultRecipient()) {
emailRecipientRepository.updateAllRecipientsToNonDefault();
}
emailRecipientMapper.updateEntityFromRequest(request, existingRecipient);
EmailRecipientEntity updatedEntity = emailRecipientRepository.save(existingRecipient);
return emailRecipientMapper.toDTO(updatedEntity);
}
@Transactional
public void setDefaultRecipient(Long id) {
EmailRecipientEntity emailRecipient = emailRecipientRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id));
emailRecipientRepository.updateAllRecipientsToNonDefault();
emailRecipient.setDefaultRecipient(true);
emailRecipientRepository.save(emailRecipient);
}
@Transactional
public void deleteEmailRecipient(Long id) {
EmailRecipientEntity emailRecipientToDelete = emailRecipientRepository.findById(id).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id));
boolean isDefaultRecipient = emailRecipientToDelete.isDefaultRecipient();
if (isDefaultRecipient) {
List<EmailRecipientEntity> allRecipients = emailRecipientRepository.findAll();
if (allRecipients.size() > 1) {
allRecipients.remove(emailRecipientToDelete);
int randomIndex = ThreadLocalRandom.current().nextInt(allRecipients.size());
EmailRecipientEntity newDefaultRecipient = allRecipients.get(randomIndex);
newDefaultRecipient.setDefaultRecipient(true);
emailRecipientRepository.save(newDefaultRecipient);
}
}
emailRecipientRepository.deleteById(id);
}
public List<EmailRecipient> getEmailRecipients() {
return emailRecipientRepository.findAll().stream()
.map(emailRecipientMapper::toDTO)
.toList();
}
}

View File

@@ -44,7 +44,7 @@ public class EmailRecipientV2Service {
BookLoreUser user = authService.getAuthenticatedUser();
boolean isFirstRecipient = repository.count() == 0;
if (request.isDefaultRecipient() || isFirstRecipient) {
repository.updateAllRecipientsToNonDefault();
repository.updateAllRecipientsToNonDefault(user.getId());
}
EmailRecipientV2Entity entity = mapper.toEntity(request);
entity.setDefaultRecipient(request.isDefaultRecipient() || isFirstRecipient);
@@ -58,7 +58,7 @@ public class EmailRecipientV2Service {
BookLoreUser user = authService.getAuthenticatedUser();
EmailRecipientV2Entity existingRecipient = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id));
if (request.isDefaultRecipient()) {
repository.updateAllRecipientsToNonDefault();
repository.updateAllRecipientsToNonDefault(user.getId());
}
mapper.updateEntityFromRequest(request, existingRecipient);
EmailRecipientV2Entity updatedEntity = repository.save(existingRecipient);
@@ -69,7 +69,7 @@ public class EmailRecipientV2Service {
public void setDefaultRecipient(Long id) {
BookLoreUser user = authService.getAuthenticatedUser();
EmailRecipientV2Entity emailRecipient = repository.findByIdAndUserId(id, user.getId()).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(id));
repository.updateAllRecipientsToNonDefault();
repository.updateAllRecipientsToNonDefault(user.getId());
emailRecipient.setDefaultRecipient(true);
repository.save(emailRecipient);
}

View File

@@ -1,177 +0,0 @@
package com.adityachandel.booklore.service.email;
import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.model.dto.request.SendBookByEmailRequest;
import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.EmailProviderEntity;
import com.adityachandel.booklore.model.entity.EmailRecipientEntity;
import com.adityachandel.booklore.model.websocket.LogNotification;
import com.adityachandel.booklore.model.websocket.Severity;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.repository.BookRepository;
import com.adityachandel.booklore.repository.EmailProviderRepository;
import com.adityachandel.booklore.repository.EmailRecipientRepository;
import com.adityachandel.booklore.service.NotificationService;
import com.adityachandel.booklore.util.SecurityContextVirtualThread;
import com.adityachandel.booklore.util.FileUtils;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.Properties;
import static com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification;
@Deprecated
@Slf4j
@Service
@AllArgsConstructor
public class EmailService {
private final EmailProviderRepository emailProviderRepository;
private final BookRepository bookRepository;
private final EmailRecipientRepository emailRecipientRepository;
private final NotificationService notificationService;
public void emailBookQuick(Long bookId) {
BookEntity book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
EmailProviderEntity defaultEmailProvider = emailProviderRepository.findDefaultEmailProvider().orElseThrow(ApiError.DEFAULT_EMAIL_PROVIDER_NOT_FOUND::createException);
EmailRecipientEntity defaultEmailRecipient = emailRecipientRepository.findDefaultEmailRecipient().orElseThrow(ApiError.DEFAULT_EMAIL_RECIPIENT_NOT_FOUND::createException);
sendEmailInVirtualThread(defaultEmailProvider, defaultEmailRecipient.getEmail(), book);
}
public void emailBook(SendBookByEmailRequest request) {
EmailProviderEntity emailProvider = emailProviderRepository.findById(request.getProviderId()).orElseThrow(() -> ApiError.EMAIL_PROVIDER_NOT_FOUND.createException(request.getProviderId()));
BookEntity book = bookRepository.findById(request.getBookId()).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(request.getBookId()));
EmailRecipientEntity emailRecipient = emailRecipientRepository.findById(request.getRecipientId()).orElseThrow(() -> ApiError.EMAIL_RECIPIENT_NOT_FOUND.createException(request.getRecipientId()));
sendEmailInVirtualThread(emailProvider, emailRecipient.getEmail(), book);
}
private void sendEmailInVirtualThread(EmailProviderEntity emailProvider, String recipientEmail, BookEntity book) {
String bookTitle = book.getMetadata().getTitle();
String logMessage = "Email dispatch initiated for book: " + bookTitle + " to " + recipientEmail;
notificationService.sendMessage(Topic.LOG, LogNotification.info(logMessage));
log.info(logMessage);
SecurityContextVirtualThread.runWithSecurityContext(() -> {
try {
sendEmail(emailProvider, recipientEmail, book);
String successMessage = "The book: " + bookTitle + " has been successfully sent to " + recipientEmail;
notificationService.sendMessage(Topic.LOG, LogNotification.info(successMessage));
log.info(successMessage);
} catch (Exception e) {
String errorMessage = "An error occurred while sending the book: " + bookTitle + " to " + recipientEmail + ". Error: " + e.getMessage();
notificationService.sendMessage(Topic.LOG, LogNotification.error(errorMessage));
log.error(errorMessage, e);
}
});
}
private void sendEmail(EmailProviderEntity emailProvider, String recipientEmail, BookEntity book) throws MessagingException {
JavaMailSenderImpl dynamicMailSender = setupMailSender(emailProvider);
MimeMessage message = dynamicMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(StringUtils.firstNonEmpty(emailProvider.getFromAddress(), emailProvider.getUsername()));
helper.setTo(recipientEmail);
helper.setSubject("Your Book from Booklore: " + book.getMetadata().getTitle());
helper.setText(generateEmailBody(book.getMetadata().getTitle()));
File bookFile = new File(FileUtils.getBookFullPath(book));
helper.addAttachment(bookFile.getName(), bookFile);
dynamicMailSender.send(message);
log.info("Book sent successfully to {}", recipientEmail);
}
private JavaMailSenderImpl setupMailSender(EmailProviderEntity emailProvider) {
JavaMailSenderImpl dynamicMailSender = new JavaMailSenderImpl();
dynamicMailSender.setHost(emailProvider.getHost());
dynamicMailSender.setPort(emailProvider.getPort());
dynamicMailSender.setUsername(emailProvider.getUsername());
dynamicMailSender.setPassword(emailProvider.getPassword());
Properties mailProps = dynamicMailSender.getJavaMailProperties();
mailProps.put("mail.smtp.auth", emailProvider.isAuth());
ConnectionType connectionType = determineConnectionType(emailProvider);
configureConnectionType(mailProps, connectionType, emailProvider);
configureTimeouts(mailProps);
String debugMode = System.getProperty("mail.debug", "false");
mailProps.put("mail.debug", debugMode);
log.info("Email configuration: Host={}, Port={}, Type={}, Timeouts=60s", emailProvider.getHost(), emailProvider.getPort(), connectionType);
return dynamicMailSender;
}
private ConnectionType determineConnectionType(EmailProviderEntity emailProvider) {
if (emailProvider.getPort() == 465) {
return ConnectionType.SSL;
} else if (emailProvider.getPort() == 587 && emailProvider.isStartTls()) {
return ConnectionType.STARTTLS;
} else if (emailProvider.isStartTls()) {
return ConnectionType.STARTTLS;
} else {
return ConnectionType.PLAIN;
}
}
private void configureConnectionType(Properties mailProps, ConnectionType connectionType, EmailProviderEntity emailProvider) {
switch (connectionType) {
case SSL -> {
mailProps.put("mail.transport.protocol", "smtps");
mailProps.put("mail.smtp.ssl.enable", "true");
mailProps.put("mail.smtp.ssl.trust", emailProvider.getHost());
mailProps.put("mail.smtp.starttls.enable", "false");
mailProps.put("mail.smtp.ssl.protocols", "TLSv1.2,TLSv1.3");
mailProps.put("mail.smtp.ssl.checkserveridentity", "false");
mailProps.put("mail.smtp.ssl.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
mailProps.put("mail.smtp.ssl.socketFactory.fallback", "false");
}
case STARTTLS -> {
mailProps.put("mail.transport.protocol", "smtp");
mailProps.put("mail.smtp.starttls.enable", "true");
mailProps.put("mail.smtp.starttls.required", "true");
mailProps.put("mail.smtp.ssl.enable", "false");
}
case PLAIN -> {
mailProps.put("mail.transport.protocol", "smtp");
mailProps.put("mail.smtp.starttls.enable", "false");
mailProps.put("mail.smtp.ssl.enable", "false");
}
}
}
private void configureTimeouts(Properties mailProps) {
String connectionTimeout = System.getProperty("mail.smtp.connectiontimeout", "60000");
String socketTimeout = System.getProperty("mail.smtp.timeout", "60000");
String writeTimeout = System.getProperty("mail.smtp.writetimeout", "60000");
mailProps.put("mail.smtp.connectiontimeout", connectionTimeout);
mailProps.put("mail.smtp.timeout", socketTimeout);
mailProps.put("mail.smtp.writetimeout", writeTimeout);
log.debug("Configured email timeouts: connection={}, socket={}, write={}",
connectionTimeout, socketTimeout, writeTimeout);
}
private String generateEmailBody(String bookTitle) {
return String.format("""
Hey there,
Youve received a new book from Booklore titled “%s” 📚
Grab a comfy spot, maybe a cup of tea ☕, and enjoy the story!
""", bookTitle);
}
private enum ConnectionType {
SSL,
STARTTLS,
PLAIN
}
}

View File

@@ -12,6 +12,7 @@ import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.repository.BookRepository;
import com.adityachandel.booklore.repository.EmailProviderV2Repository;
import com.adityachandel.booklore.repository.EmailRecipientV2Repository;
import com.adityachandel.booklore.repository.UserEmailProviderPreferenceRepository;
import com.adityachandel.booklore.service.NotificationService;
import com.adityachandel.booklore.util.FileUtils;
import com.adityachandel.booklore.util.SecurityContextVirtualThread;
@@ -35,6 +36,7 @@ import static com.adityachandel.booklore.model.websocket.LogNotification.createL
public class SendEmailV2Service {
private final EmailProviderV2Repository emailProviderRepository;
private final UserEmailProviderPreferenceRepository preferenceRepository;
private final BookRepository bookRepository;
private final EmailRecipientV2Repository emailRecipientRepository;
private final NotificationService notificationService;
@@ -43,8 +45,8 @@ public class SendEmailV2Service {
public void emailBookQuick(Long bookId) {
BookLoreUser user = authenticationService.getAuthenticatedUser();
BookEntity book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
EmailProviderV2Entity defaultEmailProvider = emailProviderRepository.findDefaultEmailProvider(user.getId()).orElseThrow(ApiError.DEFAULT_EMAIL_PROVIDER_NOT_FOUND::createException);
EmailRecipientV2Entity defaultEmailRecipient = emailRecipientRepository.findDefaultEmailRecipient().orElseThrow(ApiError.DEFAULT_EMAIL_RECIPIENT_NOT_FOUND::createException);
EmailProviderV2Entity defaultEmailProvider = getDefaultEmailProvider();
EmailRecipientV2Entity defaultEmailRecipient = emailRecipientRepository.findDefaultEmailRecipientByUserId(user.getId()).orElseThrow(ApiError.DEFAULT_EMAIL_RECIPIENT_NOT_FOUND::createException);
sendEmailInVirtualThread(defaultEmailProvider, defaultEmailRecipient.getEmail(), book);
}
@@ -173,6 +175,17 @@ public class SendEmailV2Service {
""", bookTitle);
}
private EmailProviderV2Entity getDefaultEmailProvider() {
BookLoreUser user = authenticationService.getAuthenticatedUser();
Long defaultProviderId = preferenceRepository.findByUserId(user.getId())
.map(pref -> pref.getDefaultProviderId())
.orElseThrow(ApiError.DEFAULT_EMAIL_PROVIDER_NOT_FOUND::createException);
return emailProviderRepository.findAccessibleProvider(defaultProviderId, user.getId())
.orElseThrow(ApiError.DEFAULT_EMAIL_PROVIDER_NOT_FOUND::createException);
}
private enum ConnectionType {
SSL,
STARTTLS,

View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS user_email_provider_preference
(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
default_provider_id BIGINT NOT NULL,
CONSTRAINT uq_user_id UNIQUE (user_id),
CONSTRAINT fk_user_email_preference_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT fk_user_email_preference_provider FOREIGN KEY (default_provider_id) REFERENCES email_provider_v2 (id) ON DELETE CASCADE
)

View File

@@ -15,7 +15,7 @@ import {UrlHelperService} from '../../../../../shared/service/url-helper.service
import {NgClass} from '@angular/common';
import {UserService} from '../../../../settings/user-management/user.service';
import {filter, Subject} from 'rxjs';
import {EmailService} from '../../../../settings/email/email.service';
import {EmailService} from '../../../../settings/email-v2/email.service';
import {TieredMenu} from 'primeng/tieredmenu';
import {BookSenderComponent} from '../../book-sender/book-sender.component';
import {Router} from '@angular/router';
@@ -269,7 +269,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy {
if (this.hasEmailBookPermission()) {
items.push(
{
label: 'Send Book',
label: 'Email Book',
icon: 'pi pi-envelope',
items: [{
label: 'Quick Send',

View File

@@ -21,7 +21,7 @@
<p-button
icon="pi pi-envelope"
label="Send Book"
label="Email Book"
[disabled]="!selectedProvider || !selectedRecipient"
(onClick)="sendBook()">
</p-button>

View File

@@ -2,9 +2,9 @@ import {Component, inject, OnInit} from '@angular/core';
import {Button} from 'primeng/button';
import {Select} from 'primeng/select';
import {FormsModule} from '@angular/forms';
import {EmailProvider} from '../../../settings/email/email-provider/email-provider.model';
import {EmailRecipient} from '../../../settings/email/email-recipient/email-recipient.model';
import {EmailService} from '../../../settings/email/email.service';
import {EmailProvider} from '../../../settings/email-v2/email-provider.model';
import {EmailRecipient} from '../../../settings/email-v2/email-recipient.model';
import {EmailService} from '../../../settings/email-v2/email.service';
import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog';
import {MessageService} from 'primeng/api';
import {EmailV2ProviderService} from '../../../settings/email-v2/email-v2-provider/email-v2-provider.service';

View File

@@ -12,7 +12,7 @@ import {SplitButton} from 'primeng/splitbutton';
import {ConfirmationService, MenuItem, MessageService} from 'primeng/api';
import {BookSenderComponent} from '../../../../book/components/book-sender/book-sender.component';
import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog';
import {EmailService} from '../../../../settings/email/email.service';
import {EmailService} from '../../../../settings/email-v2/email.service';
import {ShelfAssignerComponent} from '../../../../book/components/shelf-assigner/shelf-assigner.component';
import {Tooltip} from 'primeng/tooltip';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

View File

@@ -6,7 +6,7 @@ import {InputText} from 'primeng/inputtext';
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {MessageService} from 'primeng/api';
import {DynamicDialogRef} from 'primeng/dynamicdialog';
import {EmailV2ProviderService} from '../../email-v2/email-v2-provider/email-v2-provider.service';
import {EmailV2ProviderService} from '../email-v2-provider/email-v2-provider.service';
@Component({
selector: 'app-create-email-provider-dialog',

View File

@@ -5,7 +5,7 @@ import {DynamicDialogRef} from 'primeng/dynamicdialog';
import {Checkbox} from 'primeng/checkbox';
import {Button} from 'primeng/button';
import {InputText} from 'primeng/inputtext';
import {EmailV2RecipientService} from '../../email-v2/email-v2-recipient/email-v2-recipient.service';
import {EmailV2RecipientService} from '../email-v2-recipient/email-v2-recipient.service';
@Component({
selector: 'app-create-email-recipient-dialog',

View File

@@ -9,8 +9,8 @@ import {TableModule} from 'primeng/table';
import {Tooltip} from 'primeng/tooltip';
import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog';
import {EmailV2ProviderService} from './email-v2-provider.service';
import {CreateEmailProviderDialogComponent} from '../../email/create-email-provider-dialog/create-email-provider-dialog.component';
import {EmailProvider} from '../../email/email-provider/email-provider.model';
import {CreateEmailProviderDialogComponent} from '../create-email-provider-dialog/create-email-provider-dialog.component';
import {EmailProvider} from '../email-provider.model';
import {UserService} from '../../user-management/user.service';
@Component({
@@ -138,13 +138,23 @@ export class EmailV2ProviderComponent implements OnInit {
}
setDefaultProvider(provider: EmailProvider) {
this.emailProvidersService.setDefaultProvider(provider.id).subscribe(() => {
this.defaultProviderId = provider.id;
this.messageService.add({
severity: 'success',
summary: 'Default Provider Set',
detail: `${provider.name} is now the default email provider.`
});
this.emailProvidersService.setDefaultProvider(provider.id).subscribe({
next: () => {
this.defaultProviderId = provider.id;
this.messageService.add({
severity: 'success',
summary: 'Default Provider Set',
detail: `${provider.name} is now the default email provider.`
});
},
error: (err) => {
console.error('Failed to set default provider', err);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: `Failed to set ${provider.name} as the default provider. Please try again.`
});
}
});
}

View File

@@ -2,7 +2,7 @@ import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {API_CONFIG} from '../../../../core/config/api-config';
import {EmailProvider} from '../../email/email-provider/email-provider.model';
import {EmailProvider} from '../email-provider.model';
@Injectable({
providedIn: 'root'

View File

@@ -8,8 +8,8 @@ import {TableModule} from 'primeng/table';
import {Tooltip} from 'primeng/tooltip';
import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog';
import {EmailV2RecipientService} from './email-v2-recipient.service';
import {EmailRecipient} from '../../email/email-recipient/email-recipient.model';
import {CreateEmailRecipientDialogComponent} from '../../email/create-email-recipient-dialog/create-email-recipient-dialog.component';
import {EmailRecipient} from '../email-recipient.model';
import {CreateEmailRecipientDialogComponent} from '../create-email-recipient-dialog/create-email-recipient-dialog.component';
@Component({
selector: 'app-email-v2-recipient',

View File

@@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { API_CONFIG } from '../../../../core/config/api-config';
import {EmailRecipient} from '../../email/email-recipient/email-recipient.model';
import {EmailRecipient} from '../email-recipient.model';
@Injectable({
providedIn: 'root'

View File

@@ -6,7 +6,10 @@
<app-external-doc-link docType="email"></app-external-doc-link>
</h2>
<p class="settings-description">
Configure email settings to send books directly to your devices or recipients. Set up email providers (like Gmail, Outlook, or custom SMTP servers) and manage recipient email addresses. In v2, users with email permission can now set up their own email providers and recipients without interfering with other users' configurations.
Configure email settings to send books directly to your devices or recipients. Set up email providers (like Gmail, Outlook, or custom SMTP servers) and manage recipient email addresses.
</p>
<p class="migration-notice">
<strong>Migration Notice:</strong> Support for legacy email has been removed. Please migrate to Email v2 by recreating providers and recipients. In v2, users can create their own private providers and recipients. Optionally, Booklore admins can share providers with users.
</p>
</div>

View File

@@ -30,6 +30,31 @@
margin: 0;
}
.migration-notice {
margin-top: 0.5rem;
padding: 0.75rem;
background-color: var(--p-yellow-50);
border-left: 4px solid var(--p-yellow-500);
color: var(--p-yellow-900);
font-size: 0.875rem;
line-height: 1.5;
border-radius: 0 4px 4px 0;
@media (prefers-color-scheme: dark) {
background-color: rgba(245, 158, 11, 0.15);
color: var(--p-yellow-200);
border-left-color: var(--p-yellow-400);
}
strong {
color: var(--p-yellow-800);
@media (prefers-color-scheme: dark) {
color: var(--p-yellow-100);
}
}
}
.access-denied-card {
display: flex;
align-items: center;

View File

@@ -1,22 +0,0 @@
import {inject, Injectable} from '@angular/core';
import {API_CONFIG} from '../../../core/config/api-config';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class EmailV2Service {
private readonly apiUrl = `${API_CONFIG.BASE_URL}/api/v2/emails`;
private http = inject(HttpClient);
emailBook(request: { bookId: number, providerId: number, recipientId: number }): Observable<void> {
return this.http.post<void>(`${this.apiUrl}/send-book`, request);
}
emailBookQuick(bookId: number): Observable<void> {
return this.http.post<void>(`${this.apiUrl}/send-book/${bookId}`, {});
}
}

View File

@@ -1,258 +0,0 @@
<div class="main-container enclosing-container">
<div class="deprecation-banner" style="background: rgba(220, 38, 38, 0.1); color: #fca5a5; border: 1px solid rgba(220, 38, 38, 0.3); padding: 16px; margin-bottom: 16px; border-radius: 4px;">
<div style="display: flex; align-items: center; gap: 12px;">
<i class="pi pi-exclamation-triangle" style="font-size: 1.5rem; color: red;"></i>
<div>
<strong style="font-size: 1.1rem; color: darkorange;">DEPRECATED - NO LONGER FUNCTIONAL</strong>
<p style="margin: 8px 0 0 0; font-size: 0.95rem; color: #fca5a5;">
This Email feature has been deprecated and is <b>no longer operational</b>. All functionality has been disabled.
Please <b>migrate to Email v2 immediately</b> to continue sending emails.
</p>
</div>
</div>
</div>
<div class="settings-header">
<h2 class="settings-title">
<i class="pi pi-envelope"></i>
Email Providers
</h2>
<p class="settings-description">
Configure email-sending services like Gmail, Outlook, or custom SMTP servers for sending books via email. The default email provider will be used for 'Quick Book Send' located in the Book Card menu.
</p>
</div>
<div class="settings-content">
<div class="preferences-section">
<div class="section-header">
<div class="section-title-group">
<h3 class="section-title">
<i class="pi pi-server"></i>
Current Providers
</h3>
<p-button
icon="pi pi-plus"
label="Create Provider"
severity="success"
size="small"
[outlined]="true"
(onClick)="openCreateProviderDialog()"
[disabled]="true">
</p-button>
</div>
</div>
<div class="table-card">
<p-table [value]="emailProviders" [scrollable]="true" scrollHeight="flex">
<ng-template pTemplate="header">
<tr>
<th style="width: 6%;">
<div class="header-content">
<i class="pi pi-star"></i>
<span>Default</span>
</div>
</th>
<th style="width: 10%;">
<div class="header-content">
<i class="pi pi-tag"></i>
<span>Name</span>
</div>
</th>
<th style="width: 15%;">
<div class="header-content">
<i class="pi pi-server"></i>
<span>Host</span>
</div>
</th>
<th style="width: 6%;">
<div class="header-content">
<i class="pi pi-link"></i>
<span>Port</span>
</div>
</th>
<th style="width: 14%;">
<div class="header-content">
<i class="pi pi-user"></i>
<span>Username</span>
</div>
</th>
<th style="width: 12%;">
<div class="header-content">
<i class="pi pi-lock"></i>
<span>Password</span>
</div>
</th>
<th style="width: 12%;">
<div class="header-content">
<i class="pi pi-envelope"></i>
<span>From Address</span>
</div>
</th>
<th style="width: 5%;" class="permission-header">Auth</th>
<th style="width: 5%;" class="permission-header">StartTLS</th>
<th style="width: 7%;" class="actions-header">
<div class="header-content">
<i class="pi pi-pencil"></i>
<span>Edit</span>
</div>
</th>
<th style="width: 8%;" class="actions-header">
<div class="header-content">
<i class="pi pi-trash"></i>
<span>Delete</span>
</div>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-provider>
<tr>
<td class="text-center">
<p-radioButton
name="defaultProvider"
[value]="provider.id"
[(ngModel)]="defaultProviderId"
(onClick)="setDefaultProvider(provider)"
[disabled]="true">
</p-radioButton>
</td>
<td>
@if (provider.isEditing) {
<input type="text" [(ngModel)]="provider.name" class="p-inputtext w-full" size="small"/>
}
@if (!provider.isEditing) {
<span>{{ provider.name }}</span>
}
</td>
<td>
@if (provider.isEditing) {
<input type="text" [(ngModel)]="provider.host" class="p-inputtext w-full" size="small"/>
}
@if (!provider.isEditing) {
<span>{{ provider.host }}</span>
}
</td>
<td>
@if (provider.isEditing) {
<input type="number" [(ngModel)]="provider.port" class="p-inputtext w-full" size="small"/>
}
@if (!provider.isEditing) {
<span>{{ provider.port }}</span>
}
</td>
<td>
@if (provider.isEditing) {
<input type="text" [(ngModel)]="provider.username" class="p-inputtext w-full" size="small"/>
}
@if (!provider.isEditing) {
<span>{{ provider.username }}</span>
}
</td>
<td>
@if (provider.isEditing) {
<input [(ngModel)]="provider.password" class="p-inputtext w-full" size="small"/>
}
@if (!provider.isEditing) {
<span class="password-hidden">Hidden</span>
}
</td>
<td>
@if (provider.isEditing) {
<input type="text" [(ngModel)]="provider.fromAddress" class="p-inputtext w-full" size="small"/>
}
@if (!provider.isEditing) {
<span>{{ provider.fromAddress }}</span>
}
</td>
<td class="text-center">
<p-checkbox
[(ngModel)]="provider.auth"
[binary]="true"
[disabled]="true">
</p-checkbox>
</td>
<td class="text-center">
<p-checkbox
[(ngModel)]="provider.startTls"
[binary]="true"
[disabled]="true">
</p-checkbox>
</td>
<td class="actions-cell">
@if (!provider.isEditing) {
<p-button
icon="pi pi-pencil"
severity="info"
size="small"
[outlined]="true"
[rounded]="true"
(onClick)="toggleEdit(provider)"
pTooltip="Edit provider"
[disabled]="true">
</p-button>
}
@if (provider.isEditing) {
<div class="flex gap-1">
<p-button
icon="pi pi-check"
severity="success"
size="small"
[outlined]="true"
[rounded]="true"
(onClick)="saveProvider(provider)"
pTooltip="Save changes"
[disabled]="true">
</p-button>
<p-button
icon="pi pi-times"
severity="danger"
size="small"
[outlined]="true"
[rounded]="true"
(onClick)="toggleEdit(provider)"
pTooltip="Cancel"
[disabled]="true">
</p-button>
</div>
}
</td>
<td class="actions-cell">
<p-button
icon="pi pi-trash"
severity="danger"
size="small"
[outlined]="true"
[rounded]="true"
(onClick)="deleteProvider(provider)"
pTooltip="Delete provider"
[disabled]="true">
</p-button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="11">
<div class="empty-message">
<i class="pi pi-envelope"></i>
<p class="empty-title">No email providers found</p>
<p class="empty-subtitle">Create your first email provider to start sending books</p>
</div>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
</div>
</div>

View File

@@ -1,167 +0,0 @@
.enclosing-container {
border-color: var(--p-content-border-color);
background: var(--p-content-background);
}
.settings-header {
margin-top: 1rem;
}
.settings-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.25rem;
font-weight: 700;
color: var(--p-text-color);
margin: 0 0 0.75rem 0;
.pi {
color: var(--p-primary-color);
font-size: 1.25rem;
}
}
.settings-description {
color: var(--p-text-muted-color);
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: 1rem;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 2rem;
}
.preferences-section {
@media (min-width: 768px) {
padding: 0.5rem 1rem;
}
}
.section-header {
margin-bottom: 1rem;
.section-title-group {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
@media (max-width: 767px) {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
p-button {
align-self: flex-end;
}
}
}
}
.section-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: var(--p-text-color);
.pi {
color: var(--p-primary-color);
}
}
.table-card {
border: 1px solid var(--p-content-border-color);
border-radius: 8px;
overflow: hidden;
background: var(--p-content-background);
}
.p-datatable {
.p-datatable-table {
border-collapse: separate;
border-spacing: 0;
}
.p-datatable-thead > tr > th {
background: var(--p-surface-100);
border-bottom: 2px solid var(--p-content-border-color);
padding: 1rem;
font-weight: 600;
color: var(--p-text-color);
white-space: nowrap;
}
.p-datatable-tbody > tr {
transition: background-color 0.2s;
&:hover {
background: var(--p-surface-50);
}
&:last-child {
border-bottom: none;
}
}
.p-datatable-tbody > tr > td {
padding: 1rem;
border: none;
}
}
.p-datatable th .header-content {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.5rem;
}
.permission-header {
text-align: center;
font-size: 0.875rem;
padding: 0.75rem 0.5rem !important;
min-width: 80px;
width: 80px;
}
.actions-header {
text-align: center;
min-width: 80px;
}
.actions-cell {
text-align: center;
}
.password-hidden {
color: var(--p-text-muted-color);
font-style: italic;
}
.empty-message {
text-align: center;
padding: 2rem 1rem;
color: var(--p-text-muted-color);
.pi {
font-size: 2rem;
margin-bottom: 1rem;
color: var(--p-surface-400);
}
.empty-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.empty-subtitle {
font-size: 0.875rem;
}
}

View File

@@ -1,139 +0,0 @@
import {Component, inject, OnInit} from '@angular/core';
import {Button} from 'primeng/button';
import {Checkbox} from 'primeng/checkbox';
import {MessageService, PrimeTemplate} from 'primeng/api';
import {RadioButton} from 'primeng/radiobutton';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {TableModule} from 'primeng/table';
import {Tooltip} from 'primeng/tooltip';
import {EmailProvider} from './email-provider.model';
import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog';
import {EmailProviderService} from './email-provider.service';
import {CreateEmailProviderDialogComponent} from '../create-email-provider-dialog/create-email-provider-dialog.component';
@Component({
selector: 'app-email-provider',
imports: [
Button,
Checkbox,
PrimeTemplate,
RadioButton,
ReactiveFormsModule,
TableModule,
Tooltip,
FormsModule
],
templateUrl: './email-provider.component.html',
styleUrl: './email-provider.component.scss'
})
export class EmailProviderComponent implements OnInit {
emailProviders: EmailProvider[] = [];
editingProviderIds: number[] = [];
ref: DynamicDialogRef | undefined;
private dialogService = inject(DialogService);
private emailProvidersService = inject(EmailProviderService);
private messageService = inject(MessageService);
defaultProviderId: any;
ngOnInit(): void {
this.loadEmailProviders();
}
loadEmailProviders(): void {
this.emailProvidersService.getEmailProviders().subscribe({
next: (emailProviders: EmailProvider[]) => {
this.emailProviders = emailProviders.map((provider) => ({
...provider,
isEditing: false,
}));
const defaultProvider = emailProviders.find((provider) => provider.defaultProvider);
this.defaultProviderId = defaultProvider ? defaultProvider.id : null;
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to load Email Providers',
});
},
});
}
toggleEdit(provider: EmailProvider): void {
provider.isEditing = !provider.isEditing;
if (provider.isEditing) {
this.editingProviderIds.push(provider.id);
} else {
this.editingProviderIds = this.editingProviderIds.filter((id) => id !== provider.id);
}
}
saveProvider(provider: EmailProvider): void {
this.emailProvidersService.updateProvider(provider).subscribe({
next: () => {
provider.isEditing = false;
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: 'Provider updated successfully',
});
this.loadEmailProviders();
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update provider',
});
},
});
}
deleteProvider(provider: EmailProvider): void {
if (confirm(`Are you sure you want to delete provider "${provider.name}"?`)) {
this.emailProvidersService.deleteProvider(provider.id).subscribe({
next: () => {
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: `Provider "${provider.name}" deleted successfully`,
});
this.loadEmailProviders();
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to delete provider',
});
},
});
}
}
openCreateProviderDialog() {
this.ref = this.dialogService.open(CreateEmailProviderDialogComponent, {
header: 'Create Email Provider',
modal: true,
closable: true,
style: {position: 'absolute', top: '15%'},
});
this.ref.onClose.subscribe((result) => {
if (result) {
this.loadEmailProviders();
}
});
}
setDefaultProvider(provider: EmailProvider) {
this.emailProvidersService.setDefaultProvider(provider.id).subscribe(() => {
this.defaultProviderId = provider.id;
this.messageService.add({
severity: 'success',
summary: 'Default Provider Set',
detail: `${provider.name} is now the default email provider.`
});
});
}
}

View File

@@ -1,34 +0,0 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {EmailProvider} from './email-provider.model';
import {API_CONFIG} from '../../../../core/config/api-config';
@Injectable({
providedIn: 'root'
})
export class EmailProviderService {
private readonly url = `${API_CONFIG.BASE_URL}/api/v1/email/providers`;
private http = inject(HttpClient);
getEmailProviders(): Observable<EmailProvider[]> {
return this.http.get<EmailProvider[]>(this.url);
}
createEmailProvider(provider: EmailProvider): Observable<EmailProvider> {
return this.http.post<EmailProvider>(this.url, provider);
}
updateProvider(provider: EmailProvider): Observable<EmailProvider> {
return this.http.put<EmailProvider>(`${this.url}/${provider.id}`, provider);
}
deleteProvider(id: number): Observable<void> {
return this.http.delete<void>(`${this.url}/${id}`);
}
setDefaultProvider(id: number): Observable<void> {
return this.http.patch<void>(`${this.url}/${id}/set-default`, {});
}
}

View File

@@ -1,168 +0,0 @@
<div class="main-container enclosing-container">
<div class="settings-header">
<h2 class="settings-title">
<i class="pi pi-users"></i>
Recipient Emails
</h2>
<p class="settings-description">
Manage the list of recipients who will receive books via email. The 'Default' recipient will be used for 'Quick Book Send,' located in the Book Card menu.
</p>
</div>
<div class="settings-content">
<div class="preferences-section">
<div class="section-header">
<div class="section-title-group">
<h3 class="section-title">
<i class="pi pi-envelope"></i>
Current Recipients
</h3>
<p-button
icon="pi pi-plus"
label="Create Recipient"
severity="success"
size="small"
[outlined]="true"
(onClick)="openAddRecipientDialog()"
[disabled]="true">
</p-button>
</div>
</div>
<div class="table-card">
<p-table [value]="recipientEmails" [scrollable]="true" scrollHeight="flex">
<ng-template pTemplate="header">
<tr>
<th style="width: 10%;">
<div class="header-content">
<i class="pi pi-star"></i>
<span>Default</span>
</div>
</th>
<th style="width: 45%;">
<div class="header-content">
<i class="pi pi-envelope"></i>
<span>Email Address</span>
</div>
</th>
<th style="width: 30%;">
<div class="header-content">
<i class="pi pi-user"></i>
<span>Name</span>
</div>
</th>
<th style="width: 8%;" class="actions-header">
<div class="header-content">
<i class="pi pi-pencil"></i>
<span>Edit</span>
</div>
</th>
<th style="width: 7%;" class="actions-header">
<div class="header-content">
<i class="pi pi-trash"></i>
<span>Delete</span>
</div>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-recipient>
<tr>
<td class="text-center">
<p-radioButton
name="defaultRecipient"
[value]="recipient.id"
[(ngModel)]="defaultRecipientId"
(onClick)="setDefaultRecipient(recipient)"
[disabled]="true">
</p-radioButton>
</td>
<td>
@if (recipient.isEditing) {
<input type="email" [(ngModel)]="recipient.email" class="p-inputtext w-full" size="small"/>
}
@if (!recipient.isEditing) {
<span>{{ recipient.email }}</span>
}
</td>
<td>
@if (recipient.isEditing) {
<input type="text" [(ngModel)]="recipient.name" class="p-inputtext w-full" size="small"/>
}
@if (!recipient.isEditing) {
<span>{{ recipient.name }}</span>
}
</td>
<td class="actions-cell">
@if (!recipient.isEditing) {
<p-button
icon="pi pi-pencil"
severity="info"
size="small"
[outlined]="true"
[rounded]="true"
(onClick)="toggleEditRecipient(recipient)"
pTooltip="Edit recipient"
[disabled]="true">
</p-button>
}
@if (recipient.isEditing) {
<div class="flex gap-1">
<p-button
icon="pi pi-check"
severity="success"
size="small"
[outlined]="true"
[rounded]="true"
(onClick)="saveRecipient(recipient)"
pTooltip="Save changes"
[disabled]="true">
</p-button>
<p-button
icon="pi pi-times"
severity="danger"
size="small"
[outlined]="true"
[rounded]="true"
(onClick)="toggleEditRecipient(recipient)"
pTooltip="Cancel"
[disabled]="true">
</p-button>
</div>
}
</td>
<td class="actions-cell">
<p-button
icon="pi pi-trash"
severity="danger"
size="small"
[outlined]="true"
[rounded]="true"
(onClick)="deleteRecipient(recipient)"
pTooltip="Delete recipient"
[disabled]="true">
</p-button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="5">
<div class="empty-message">
<i class="pi pi-users"></i>
<p class="empty-title">No recipients found</p>
<p class="empty-subtitle">Add your first email recipient to start sending books</p>
</div>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
</div>
</div>

View File

@@ -1,144 +0,0 @@
.enclosing-container {
border-color: var(--p-content-border-color);
background: var(--p-content-background);
}
.settings-header {
margin-top: 1rem;
}
.settings-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.25rem;
font-weight: 700;
color: var(--p-text-color);
margin: 0 0 0.75rem 0;
.pi {
color: var(--p-primary-color);
font-size: 1.25rem;
}
}
.settings-description {
color: var(--p-text-muted-color);
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: 1rem;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 2rem;
}
.preferences-section {
@media (min-width: 768px) {
padding: 0 1rem;
}
}
.section-header {
margin-bottom: 1rem;
.section-title-group {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
}
.section-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: var(--p-text-color);
.pi {
color: var(--p-primary-color);
}
}
.table-card {
border: 1px solid var(--p-content-border-color);
border-radius: 8px;
overflow: hidden;
background: var(--p-content-background);
}
.p-datatable {
.p-datatable-table {
border-collapse: separate;
border-spacing: 0;
}
.p-datatable-thead > tr > th {
background: var(--p-surface-100);
border-bottom: 2px solid var(--p-content-border-color);
padding: 1rem;
font-weight: 600;
color: var(--p-text-color);
white-space: nowrap;
}
.p-datatable-tbody > tr {
transition: background-color 0.2s;
&:hover {
background: var(--p-surface-50);
}
&:last-child {
border-bottom: none;
}
}
.p-datatable-tbody > tr > td {
padding: 1rem;
border: none;
}
}
.p-datatable th .header-content {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.5rem;
}
.actions-header {
text-align: center;
min-width: 80px;
}
.actions-cell {
text-align: center;
}
.empty-message {
text-align: center;
padding: 2rem 1rem;
color: var(--p-text-muted-color);
.pi {
font-size: 2rem;
margin-bottom: 1rem;
color: var(--p-surface-400);
}
.empty-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.empty-subtitle {
font-size: 0.875rem;
}
}

View File

@@ -1,140 +0,0 @@
import {Component, inject, OnInit} from '@angular/core';
import {Button} from 'primeng/button';
import {MessageService, PrimeTemplate} from 'primeng/api';
import {RadioButton} from 'primeng/radiobutton';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {TableModule} from 'primeng/table';
import {Tooltip} from 'primeng/tooltip';
import {EmailProvider} from '../email-provider/email-provider.model';
import {EmailRecipient} from './email-recipient.model';
import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog';
import {EmailProviderService} from '../email-provider/email-provider.service';
import {EmailRecipientService} from './email-recipient.service';
import {CreateEmailProviderDialogComponent} from '../create-email-provider-dialog/create-email-provider-dialog.component';
import {CreateEmailRecipientDialogComponent} from '../create-email-recipient-dialog/create-email-recipient-dialog.component';
@Component({
selector: 'app-email-recipient',
imports: [
Button,
PrimeTemplate,
RadioButton,
ReactiveFormsModule,
TableModule,
Tooltip,
FormsModule
],
templateUrl: './email-recipient.component.html',
styleUrl: './email-recipient.component.scss'
})
export class EmailRecipientComponent implements OnInit {
recipientEmails: EmailRecipient[] = [];
editingRecipientIds: number[] = [];
ref: DynamicDialogRef | undefined;
private dialogService = inject(DialogService);
private emailRecipientService = inject(EmailRecipientService);
private messageService = inject(MessageService);
defaultRecipientId: any;
ngOnInit(): void {
this.loadRecipientEmails();
}
loadRecipientEmails(): void {
this.emailRecipientService.getRecipients().subscribe({
next: (recipients: EmailRecipient[]) => {
this.recipientEmails = recipients.map((recipient) => ({
...recipient,
isEditing: false,
}));
const defaultRecipient = recipients.find((recipient) => recipient.defaultRecipient);
this.defaultRecipientId = defaultRecipient ? defaultRecipient.id : null;
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to load recipient emails',
});
},
});
}
toggleEditRecipient(recipient: EmailRecipient): void {
recipient.isEditing = !recipient.isEditing;
if (recipient.isEditing) {
this.editingRecipientIds.push(recipient.id);
} else {
this.editingRecipientIds = this.editingRecipientIds.filter((id) => id !== recipient.id);
}
}
saveRecipient(recipient: EmailRecipient): void {
this.emailRecipientService.updateRecipient(recipient).subscribe({
next: () => {
recipient.isEditing = false;
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: 'Recipient updated successfully',
});
this.loadRecipientEmails();
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to update recipient',
});
},
});
}
deleteRecipient(recipient: EmailRecipient): void {
if (confirm(`Are you sure you want to delete recipient "${recipient.email}"?`)) {
this.emailRecipientService.deleteRecipient(recipient.id).subscribe({
next: () => {
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: `Recipient "${recipient.email}" deleted successfully`,
});
this.loadRecipientEmails();
},
error: () => {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to delete recipient',
});
},
});
}
}
openAddRecipientDialog() {
this.ref = this.dialogService.open(CreateEmailRecipientDialogComponent, {
header: 'Add New Recipient',
modal: true,
closable: true,
style: {position: 'absolute', top: '15%'},
});
this.ref.onClose.subscribe((result) => {
if (result) {
this.loadRecipientEmails();
}
});
}
setDefaultRecipient(recipient: EmailRecipient) {
this.emailRecipientService.setDefaultRecipient(recipient.id).subscribe(() => {
this.defaultRecipientId = recipient.id;
this.messageService.add({
severity: 'success',
summary: 'Default Recipient Set',
detail: `${recipient.email} is now the default recipient.`
});
});
}
}

View File

@@ -1,34 +0,0 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { API_CONFIG } from '../../../../core/config/api-config';
import {EmailRecipient} from './email-recipient.model';
@Injectable({
providedIn: 'root'
})
export class EmailRecipientService {
private readonly url = `${API_CONFIG.BASE_URL}/api/v1/email/recipients`;
private http = inject(HttpClient);
getRecipients(): Observable<EmailRecipient[]> {
return this.http.get<EmailRecipient[]>(this.url);
}
createRecipient(recipient: EmailRecipient): Observable<EmailRecipient> {
return this.http.post<EmailRecipient>(this.url, recipient);
}
updateRecipient(recipient: EmailRecipient): Observable<EmailRecipient> {
return this.http.put<EmailRecipient>(`${this.url}/${recipient.id}`, recipient);
}
deleteRecipient(id: number): Observable<void> {
return this.http.delete<void>(`${this.url}/${id}`);
}
setDefaultRecipient(id: number): Observable<void> {
return this.http.patch<void>(`${this.url}/${id}/set-default`, {});
}
}

View File

@@ -1,9 +0,0 @@
<div class="w-full h-[calc(100dvh-10.5rem)] md:h-[calc(100dvh-11.65rem)] overflow-y-auto border rounded-lg p-4 enclosing-container">
<div class="pb-8">
<app-email-provider></app-email-provider>
</div>
<p-divider></p-divider>
<div class="pt-4">
<app-email-recipient></app-email-recipient>
</div>
</div>

View File

@@ -1,3 +0,0 @@
.enclosing-container {
border-color: var(--p-content-border-color);
}

View File

@@ -1,22 +0,0 @@
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {TableModule} from 'primeng/table';
import {EmailProviderComponent} from './email-provider/email-provider.component';
import {EmailRecipientComponent} from './email-recipient/email-recipient.component';
import {Divider} from 'primeng/divider';
@Component({
selector: 'app-email',
imports: [
FormsModule,
TableModule,
EmailProviderComponent,
EmailRecipientComponent,
Divider
],
templateUrl: './email.component.html',
styleUrls: ['./email.component.scss'],
})
export class EmailComponent {
}

View File

@@ -20,10 +20,7 @@
<i class="pi pi-cog"></i> Application
</p-tab>
<p-tab [value]="SettingsTab.UserManagement">
<i class="pi pi-users"></i> User
</p-tab>
<p-tab [value]="SettingsTab.EmailSettings">
<i class="pi pi-envelope"></i> Email
<i class="pi pi-users"></i> Users
</p-tab>
}
<p-tab [value]="SettingsTab.EmailSettingsV2">
@@ -68,9 +65,6 @@
<p-tabpanel [value]="SettingsTab.UserManagement">
<app-user-management></app-user-management>
</p-tabpanel>
<p-tabpanel [value]="SettingsTab.EmailSettings">
<app-email></app-email>
</p-tabpanel>
}
<p-tabpanel [value]="SettingsTab.EmailSettingsV2">
<app-email-v2></app-email-v2>

View File

@@ -2,7 +2,6 @@ import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {Tab, TabList, TabPanel, TabPanels, Tabs} from 'primeng/tabs';
import {UserService} from './user-management/user.service';
import {AsyncPipe} from '@angular/common';
import {EmailComponent} from './email/email.component';
import {GlobalPreferencesComponent} from './global-preferences/global-preferences.component';
import {ActivatedRoute, Router} from '@angular/router';
import {Subscription} from 'rxjs';
@@ -23,7 +22,6 @@ export enum SettingsTab {
ViewPreferences = 'view',
DeviceSettings = 'device',
UserManagement = 'user',
EmailSettings = 'email',
EmailSettingsV2 = 'email-v2',
NamingPattern = 'naming-pattern',
MetadataSettings = 'metadata',
@@ -31,7 +29,7 @@ export enum SettingsTab {
ApplicationSettings = 'application',
AuthenticationSettings = 'authentication',
OpdsV2 = 'opds',
Tasks = 'tasks',
Tasks = 'task',
}
@Component({
@@ -43,7 +41,6 @@ export enum SettingsTab {
TabPanels,
TabPanel,
AsyncPipe,
EmailComponent,
GlobalPreferencesComponent,
UserManagementComponent,
AuthenticationSettingsComponent,