Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using Volo.Abp.Application.Dtos;

namespace Unity.Notifications.Emails;

[Serializable]
public class EmailLogAttachmentDto : EntityDto<Guid>
{
public string? FileName { get; set; }
public string? DisplayName { get; set; }
public DateTime Time { get; set; }
public long FileSize { get; set; }
public string ContentType { get; set; } = string.Empty;
public string S3ObjectKey { get; set; } = string.Empty;
public string AttachedBy { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;

namespace Unity.Notifications.Emails;

public interface IEmailLogAttachmentAppService : IApplicationService
{
Task<List<EmailLogAttachmentDto>> GetListByEmailLogIdAsync(Guid emailLogId);
Task DeleteAsync(Guid id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;
using System.Threading.Tasks;

namespace Unity.Notifications.Emails;

public interface IEmailLogAttachmentUploadService
{
Task<EmailLogAttachmentDto> UploadAsync(Guid emailLogId, Guid? tenantId, string fileName, byte[] content, string contentType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,36 @@ public class EmailNotificationManager(
}
}

public async Task<EmailLog> CreateDraftEmailLogAsync(Guid applicationId)
{
var emailLog = new EmailLog
{
ApplicationId = applicationId,
Status = EmailStatus.Draft
};
return await emailLogsRepository.InsertAsync(emailLog, autoSave: true);
}

public async Task DeleteEmailLogAsync(Guid id)
{
var emailLog = await emailLogsRepository.GetAsync(id);
if (emailLog.Status == EmailStatus.Sent)
{
throw new UserFriendlyException("Sent emails cannot be deleted.");
}

var attachments = await emailAttachmentService.GetAttachmentsAsync(id);
foreach (var s3Key in attachments.Select(attachment => attachment.S3ObjectKey))
{
try
{
await emailAttachmentService.DeleteFromS3Async(s3Key);
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to delete S3 attachment for EmailLog {EmailLogId}", id);
}
}
await emailLogsRepository.DeleteAsync(id);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public class EmailNotificationService(
IFeatureChecker featureChecker) : ApplicationService, IEmailNotificationService
{

public async Task<Guid> InitializeDraftAsync(Guid applicationId)
{
var emailLog = await emailNotificationManager.CreateDraftEmailLogAsync(applicationId);
return emailLog.Id;
}

public async Task DeleteEmail(Guid id)
{
await emailNotificationManager.DeleteEmailLogAsync(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ public interface IEmailNotificationManager
Task<EmailLog?> GetEmailLogByIdAsync(Guid id);

/// <summary>
/// Deletes an email log
/// Creates an empty draft email log for composing
/// </summary>
Task<EmailLog> CreateDraftEmailLogAsync(Guid applicationId);

/// <summary>
/// Deletes an email log and its S3 attachments
/// </summary>
Task DeleteEmailLogAsync(Guid id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public interface IEmailNotificationService : IApplicationService
Task SendEmailToQueue(EmailLog emailLog);
Task<List<EmailHistoryDto>> GetHistoryByApplicationId(Guid applicationId);
Task UpdateSettings(NotificationsSettingsDto settingsDto);
Task<Guid> InitializeDraftAsync(Guid applicationId);
Task DeleteEmail(Guid id);
Task<int> GetEmailsChesWithNoResponseCountAsync();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace Unity.Notifications.EmailNotifications;

public class EmailAttachmentService : ITransientDependency
{
private const string S3BucketConfigKey = "S3:Bucket";

private readonly AmazonS3Client _amazonS3Client;
private readonly IEmailLogAttachmentRepository _emailLogAttachmentRepository;
private readonly IConfiguration _configuration;
Expand Down Expand Up @@ -55,7 +57,7 @@ public async Task<EmailLogAttachment> UploadAttachmentAsync(
string contentType)
{
var s3Key = BuildS3Key(tenantId, emailLogId, fileName);
var bucket = _configuration["S3:Bucket"];
var bucket = _configuration[S3BucketConfigKey];

// Upload to S3
using var uploadStream = new MemoryStream(fileContent);
Expand Down Expand Up @@ -94,7 +96,7 @@ public async Task<EmailLogAttachment> UploadAttachmentAsync(

public async Task<byte[]?> DownloadFromS3Async(string s3ObjectKey)
{
var bucket = _configuration["S3:Bucket"];
var bucket = _configuration[S3BucketConfigKey];

var getObjectRequest = new GetObjectRequest
{
Expand All @@ -111,11 +113,76 @@ public async Task<EmailLogAttachment> UploadAttachmentAsync(
return memoryStream.ToArray();
}

public async Task<EmailLogAttachment> UploadUserAttachmentAsync(
Guid emailLogId,
Guid? tenantId,
string fileName,
byte[] fileContent,
string contentType)
{
var uniqueKey = Guid.NewGuid();
var s3Key = BuildUserAttachmentS3Key(tenantId, emailLogId, uniqueKey, fileName);
var bucket = _configuration[S3BucketConfigKey];

using var uploadStream = new MemoryStream(fileContent);
var putRequest = new PutObjectRequest
{
BucketName = bucket,
Key = s3Key,
ContentType = contentType,
InputStream = uploadStream,
UseChunkEncoding = false,
DisablePayloadSigning = false
};

await _amazonS3Client.PutObjectAsync(putRequest);
_logger.LogInformation(
"Uploaded user email attachment to S3: FileName={FileName}, FileSize={FileSize}",
fileName, fileContent.Length);

var attachment = new EmailLogAttachment
{
EmailLogId = emailLogId,
S3ObjectKey = s3Key,
FileName = fileName,
DisplayName = fileName,
ContentType = contentType,
FileSize = fileContent.Length,
Time = DateTime.UtcNow,
UserId = _currentUser.Id ?? Guid.Empty,
TenantId = tenantId
};

await _emailLogAttachmentRepository.InsertAsync(attachment);
return attachment;
}

public async Task DeleteFromS3Async(string s3ObjectKey)
{
var bucket = _configuration[S3BucketConfigKey];
var deleteRequest = new DeleteObjectRequest
{
BucketName = bucket,
Key = s3ObjectKey
};
await _amazonS3Client.DeleteObjectAsync(deleteRequest);
_logger.LogInformation("Deleted email attachment from S3.");
}

public async Task<List<EmailLogAttachment>> GetAttachmentsAsync(Guid emailLogId)
{
return await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId);
}

private static string BuildUserAttachmentS3Key(Guid? tenantId, Guid emailLogId, Guid attachmentId, string fileName)
{
var basePath = "Email/Attachments";
var tenantPart = tenantId?.ToString() ?? "host";
var escapedFileName = Uri.EscapeDataString(fileName);

return $"{basePath}/{tenantPart}/{emailLogId}/{attachmentId}/{escapedFileName}";
}

private static string BuildS3Key(Guid? tenantId, Guid emailLogId, string fileName)
{
var basePath = "Email/FSB-AP-Payments";
Expand All @@ -124,6 +191,4 @@ private static string BuildS3Key(Guid? tenantId, Guid emailLogId, string fileNam

return $"{basePath}/{tenantPart}/{emailLogId}/{escapedFileName}";
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Notifications.EmailNotifications;
using Unity.Notifications.Permissions;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Users;

namespace Unity.Notifications.Emails;

[Authorize(NotificationsPermissions.Email.Send)]
[ExposeServices(typeof(EmailLogAttachmentAppService), typeof(IEmailLogAttachmentAppService), typeof(IEmailLogAttachmentUploadService))]
public class EmailLogAttachmentAppService(
IEmailLogAttachmentRepository emailLogAttachmentRepository,
IEmailLogsRepository emailLogsRepository,
EmailAttachmentService emailAttachmentService,
IExternalUserLookupServiceProvider externalUserLookupServiceProvider) : ApplicationService, IEmailLogAttachmentAppService, IEmailLogAttachmentUploadService
{
public async Task<List<EmailLogAttachmentDto>> GetListByEmailLogIdAsync(Guid emailLogId)
{
var attachments = await emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId);
var dtos = new List<EmailLogAttachmentDto>();

foreach (var attachment in attachments)
{
var dto = new EmailLogAttachmentDto
{
Id = attachment.Id,
FileName = attachment.FileName,
DisplayName = attachment.DisplayName,
Time = attachment.Time,
FileSize = attachment.FileSize,
ContentType = attachment.ContentType,
S3ObjectKey = attachment.S3ObjectKey,
AttachedBy = await ResolveUserNameAsync(attachment.UserId)
};
dtos.Add(dto);
}

return dtos;
}

public async Task DeleteAsync(Guid id)
{
var attachment = await emailLogAttachmentRepository.GetAsync(id);

var emailLog = await emailLogsRepository.GetAsync(attachment.EmailLogId);
if (emailLog.Status != EmailStatus.Draft)
{
throw new UserFriendlyException("Attachments can only be deleted from draft emails.");
}

try
{
await emailAttachmentService.DeleteFromS3Async(attachment.S3ObjectKey);
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to delete S3 object {S3ObjectKey} for attachment {AttachmentId}", attachment.S3ObjectKey, id);
}
await emailLogAttachmentRepository.DeleteAsync(id);
}

public async Task<EmailLogAttachmentDto> UploadAsync(Guid emailLogId, Guid? tenantId, string fileName, byte[] content, string contentType)
{
var attachment = await emailAttachmentService.UploadUserAttachmentAsync(emailLogId, tenantId, fileName, content, contentType);

return new EmailLogAttachmentDto
{
Id = attachment.Id,
FileName = attachment.FileName,
DisplayName = attachment.DisplayName,
Time = attachment.Time,
FileSize = attachment.FileSize,
ContentType = attachment.ContentType,
S3ObjectKey = attachment.S3ObjectKey,
AttachedBy = await ResolveUserNameAsync(attachment.UserId)
};
}

private async Task<string> ResolveUserNameAsync(Guid userId)
{
try
{
var user = await externalUserLookupServiceProvider.FindByIdAsync(userId);
if (user == null) return string.Empty;

var fullName = $"{user.Name} {user.Surname}".Trim();
return string.IsNullOrEmpty(fullName) ? user.UserName : fullName;
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to resolve username for UserId {UserId}", userId);
return string.Empty;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;

namespace Unity.GrantManager.Emails
{
public interface IEmailAppService
{
Task<bool> CreateAsync(CreateEmailDto dto);
Task<Guid> InitializeDraftAsync(Guid applicationId);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Authorization;
using System;
using System.Threading.Tasks;
using Unity.Modules.Shared.Utils;
using Unity.Notifications.EmailNotifications;
using Unity.Notifications.Emails;
using Unity.Notifications.Events;
using Volo.Abp.Application.Services;
Expand All @@ -12,8 +14,12 @@ namespace Unity.GrantManager.Emails
[Authorize]
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(EmailAppService), typeof(IEmailAppService))]
public class EmailAppService(ILocalEventBus localEventBus) : ApplicationService, IEmailAppService
public class EmailAppService(ILocalEventBus localEventBus, IEmailNotificationService emailNotificationService) : ApplicationService, IEmailAppService
{
public async Task<Guid> InitializeDraftAsync(Guid applicationId)
{
return await emailNotificationService.InitializeDraftAsync(applicationId);
}
public async Task<bool> CreateAsync(CreateEmailDto dto)
{
EmailNotificationEvent emailNotificationEvent = GetEmailNotificationEvent(dto);
Expand Down
Loading
Loading