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
2 changes: 2 additions & 0 deletions applications/Unity.GrantManager/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ AuthServer__OidcSignoutCallback="http://localhost:44342/signout-callback-oidc"
##S3__AssessmentS3Folder="Unity/Adjudication"
##S3__DisallowedFileTypes="[ "exe" , "sh" , "ksh" , "bat" , "cmd" ]"
##S3__MaxFileSize="25"
##S3__EmailAttachmentMaxFileSize="20"
##S3__EmailAttachmentsTotalMaxFileSize="25"
## RABBIT MQ
UNITY_RABBIT_MQ_HOST="rabbitmq"
UNITY_RABBIT_MQ_PORT="5672"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ namespace Unity.Notifications.Emails;
public interface IEmailLogAttachmentUploadService
{
Task<EmailLogAttachmentDto> UploadAsync(Guid emailLogId, Guid? tenantId, string fileName, byte[] content, string contentType);
Task<long> GetTotalFileSizeByEmailLogIdAsync(Guid emailLogId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Unity.Notifications.Emails;
using Volo.Abp.DependencyInjection;
Expand Down Expand Up @@ -174,6 +175,12 @@ public async Task<List<EmailLogAttachment>> GetAttachmentsAsync(Guid emailLogId)
return await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId);
}

public async Task<long> GetTotalFileSizeAsync(Guid emailLogId)
{
var attachments = await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId);
return attachments.Sum(a => a.FileSize);
}

private static string BuildUserAttachmentS3Key(Guid? tenantId, Guid emailLogId, Guid attachmentId, string fileName)
{
var basePath = "Email/Attachments";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async Task<List<EmailLogAttachmentDto>> GetListByEmailLogIdAsync(Guid ema

foreach (var attachment in attachments)
{
var dto = new EmailLogAttachmentDto
dtos.Add(new EmailLogAttachmentDto
{
Id = attachment.Id,
FileName = attachment.FileName,
Expand All @@ -37,8 +37,7 @@ public async Task<List<EmailLogAttachmentDto>> GetListByEmailLogIdAsync(Guid ema
ContentType = attachment.ContentType,
S3ObjectKey = attachment.S3ObjectKey,
AttachedBy = await ResolveUserNameAsync(attachment.UserId)
};
dtos.Add(dto);
});
}

return dtos;
Expand All @@ -65,6 +64,11 @@ public async Task DeleteAsync(Guid id)
await emailLogAttachmentRepository.DeleteAsync(id);
}

public async Task<long> GetTotalFileSizeByEmailLogIdAsync(Guid emailLogId)
{
return await emailAttachmentService.GetTotalFileSizeAsync(emailLogId);
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,35 @@ public async Task<IActionResult> UploadEmailAttachments(Guid emailLogId, IList<I
throw new AbpValidationException(message: "ERROR: Invalid File Type.", validationErrors: invalidFileTypes);
}

var emailAttachmentMaxFileSizeConfig = _configuration["S3:EmailAttachmentMaxFileSize"] ?? "20";
if (double.TryParse(emailAttachmentMaxFileSizeConfig, out double maxFileSizeMB))
{
var oversizedFiles = files.Where(f => f.Length * 0.000001 > maxFileSizeMB).ToList();
if (oversizedFiles.Count > 0)
{
var sizeErrors = oversizedFiles.Select(f =>
new ValidationResult($"File '{f.FileName}' exceeds the maximum allowed size of {maxFileSizeMB} MB for email attachments.", [f.FileName])
).ToList();
throw new AbpValidationException("One or more files exceed the maximum allowed size for email attachments.", sizeErrors);
}
}

var totalMaxFileSizeConfig = _configuration["S3:EmailAttachmentsTotalMaxFileSize"] ?? "25";
if (double.TryParse(totalMaxFileSizeConfig, out double totalMaxSizeMB))
{
long existingTotalBytes = await _emailLogAttachmentUploadService
.GetTotalFileSizeByEmailLogIdAsync(emailLogId);
long newFilesBytes = files.Sum(f => f.Length);
double combinedMB = (existingTotalBytes + newFilesBytes) * 0.000001;

if (combinedMB > totalMaxSizeMB)
{
throw new AbpValidationException(
$"The total size of all attachments ({combinedMB:F1} MB) would exceed the maximum allowed {totalMaxSizeMB} MB for email attachments. Please remove existing attachments or select a smaller file.",
[new ValidationResult("Total attachment size exceeds the allowed limit.")]);
}
}

var results = new List<object>();
foreach (var file in files)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
<input type="hidden" id="CurrentUserName" value="@Model.CurrentUserName" />
<input type="hidden" id="Extensions" value="@Model.Extensions" />
<input type="hidden" id="MaxFileSize" value="@Model.MaxFileSize" />
<input type="hidden" id="EmailAttachmentMaxFileSize" value="@Model.EmailAttachmentMaxFileSize" />
<input type="hidden" id="TotalEmailAttachmentMaxFileSize" value="@Model.TotalEmailAttachmentMaxFileSize" />
<input type="hidden" id="RenderFormIoToHtml" value="@Model.RenderFormIoToHtml.ToString()" />
<input type="hidden" id="ApplicationFormVersionId" value="@Model.ApplicationFormVersionId" />
<input type="hidden" id="AIAnalysisFeatureEnabled" value="@aiApplicationAnalysisEnabled" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public class DetailsModel : AbpPageModel
public string? CurrentUserName { get; set; }
public string Extensions { get; set; }
public string MaxFileSize { get; set; }
public string EmailAttachmentMaxFileSize { get; set; }
public string TotalEmailAttachmentMaxFileSize { get; set; }

[BindProperty(SupportsGet = true)]
public List<BoundWorksheet> CustomTabs { get; set; } = [];
Expand Down Expand Up @@ -123,6 +125,8 @@ public DetailsModel(
CurrentUserName = currentUser.SurName + ", " + currentUser.Name;
Extensions = configuration["S3:DisallowedFileTypes"] ?? "";
MaxFileSize = configuration["S3:MaxFileSize"] ?? "";
EmailAttachmentMaxFileSize = configuration["S3:EmailAttachmentMaxFileSize"] ?? "20";
TotalEmailAttachmentMaxFileSize = configuration["S3:EmailAttachmentsTotalMaxFileSize"] ?? "25";
IsDevPromptControlsEnabled = aiPromptToolViewOptionsProvider.IsDevPromptControlsEnabled;
DefaultPromptVersion = aiPromptToolViewOptionsProvider.DefaultPromptVersion;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,25 @@
<abp-table id="EmailAttachmentsTable"
hoverable-rows="true"
class="attachments-table nowrap"></abp-table>
<div id="attachment-upload-progress" style="display:none" class="mt-2 mb-1">
<div class="progress">
<div id="attachment-upload-progress-bar"
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width:0%"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100">0%</div>
</div>
</div>
<div id="email-attachment-size-error" class="alert alert-danger mt-2" style="display:none">
<i class="fa fa-exclamation-triangle me-1"></i>
<span id="email-attachment-size-error-message"></span>
</div>
<div class="d-flex justify-content-end mt-2 mb-1">
<form id="email-attachment-form" enctype="multipart/form-data">
<input id="email_attachment_upload" type="file" multiple
style="display:none"
onchange="uploadEmailFiles('email_attachment_upload');" />
style="display:none" />
<abp-button text="Add Attachments"
id="email_attachment_upload_btn"
icon-type="Other"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,17 @@
className: 'data-table-header',
width: '25%'
},
{
title: 'File Size',
data: 'fileSize',
className: 'data-table-header',
width: '90px',
render: function (data) {
if (!data) return '—';
const mb = data * 0.000001;
return mb >= 1 ? mb.toFixed(2) + ' MB' : (data / 1024).toFixed(0) + ' KB';
}
},
{
title: '',
data: 'id',
Expand All @@ -784,11 +795,44 @@
return isDraft ? generateEmailAttachmentButtonContent(data) : '';
}
}
]
],
drawCallback: function () {
if (isDraft) {
checkTotalAttachmentSize();
}
}
})
);
}

function checkTotalAttachmentSize() {
const totalMaxFileSize = Number.parseFloat(
decodeURIComponent($('#TotalEmailAttachmentMaxFileSize').val()) || '25'
);

let totalBytes = 0;
if (emailAttachmentsTable) {
emailAttachmentsTable.rows().data().each(function (row) {
totalBytes += (row.fileSize || 0);
});
}

const totalMB = totalBytes * 0.000001;
const isExceeded = totalMB > totalMaxFileSize;

if (isExceeded) {
$('#email-attachment-size-error-message').text(
'The total size of all attachments (' + totalMB.toFixed(2) + ' MB) exceeds the ' +
'maximum allowed ' + totalMaxFileSize + ' MB. Please remove one or more attachments before sending.'
);
$('#email-attachment-size-error').show();
$('#btn-send').prop('disabled', true);
} else {
$('#email-attachment-size-error').hide();
$('#btn-send').prop('disabled', false);
}
}

function reloadEmailAttachmentsTable() {
if (emailAttachmentsTable) {
emailAttachmentsTable.ajax.reload();
Expand All @@ -798,6 +842,110 @@
$('#email_attachment_upload_btn').on('click', function () {
$('#email_attachment_upload').click();
});

$('#email_attachment_upload').on('change', function () {
uploadEmailFiles('email_attachment_upload');
});

function uploadEmailFiles(inputId) {
const emailLogId = $('#EmailId').val();
const input = document.getElementById(inputId);
if (!input?.files?.length) return;

const disallowedTypes = JSON.parse(decodeURIComponent($('#Extensions').val()));
const maxFileSize = decodeURIComponent($('#EmailAttachmentMaxFileSize').val());

let isAllowedTypeError = false;
let isMaxFileSizeError = false;
const formData = new FormData();

for (let file of input.files) {
const ext = file.name.slice(file.name.lastIndexOf('.') + 1).toLowerCase();
if (disallowedTypes.includes(ext)) {
isAllowedTypeError = true;
}
if (file.size * 0.000001 > maxFileSize) {
isMaxFileSizeError = true;
}
formData.append('files', file);
}

if (isAllowedTypeError) {
input.value = '';
return abp.notify.error('Error', 'File type not supported');
}
if (isMaxFileSizeError) {
input.value = '';
return abp.notify.error(
'File Too Large',
'The selected file exceeds the maximum allowed size of ' + maxFileSize + ' MB for email attachments. Please select a smaller file.'
);
}

const totalMaxFileSize = Number.parseFloat(
decodeURIComponent($('#TotalEmailAttachmentMaxFileSize').val()) || '25'
);
let existingTotalBytes = 0;
if (emailAttachmentsTable) {
emailAttachmentsTable.rows().data().each(function (row) {
existingTotalBytes += (row.fileSize || 0);
});
}
let newFilesBytes = 0;
for (let file of input.files) {
newFilesBytes += file.size;
}
const combinedMB = (existingTotalBytes + newFilesBytes) * 0.000001;
if (combinedMB > totalMaxFileSize) {
input.value = '';
return abp.notify.error(
'Total Size Exceeded',
'The total size of all attachments would exceed the maximum allowed ' + totalMaxFileSize +
' MB. Please remove existing attachments or select a smaller file.'
);
}

$.ajax({
url: `/api/app/attachment/email/${emailLogId}/upload`,
type: 'POST',
data: formData,
processData: false,
contentType: false,
xhr: function () {
const xhr = new globalThis.XMLHttpRequest();
xhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 100);
$('#attachment-upload-progress-bar')
.css('width', pct + '%')
.attr('aria-valuenow', pct)
.text(pct + '%');
}
});
return xhr;
},
beforeSend: function () {
$('#email_attachment_upload_btn')
.html('<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>Uploading...')
.prop('disabled', true);
$('#attachment-upload-progress-bar').css('width', '0%').text('0%');
$('#attachment-upload-progress').show();
},
success: function () {
PubSub.publish('reload_email_attachments_table');
},
error: function (xhr) {
abp.notify.error(xhr.responseText || 'Failed to upload attachment.');
},
complete: function () {
input.value = '';
$('#email_attachment_upload_btn')
.html('<i class="fl fl-plus me-1"></i>Add Attachments')
.prop('disabled', false);
$('#attachment-upload-progress').hide();
}
});
}
});

function generateEmailAttachmentButtonContent(attachmentId) {
Expand All @@ -815,56 +963,6 @@ function generateEmailAttachmentButtonContent(attachmentId) {
</div>`;
}

function uploadEmailFiles(inputId) {
const emailLogId = $('#EmailId').val();
const input = document.getElementById(inputId);
if (!input?.files?.length) return;

const disallowedTypes = JSON.parse(decodeURIComponent($('#Extensions').val()));
const maxFileSize = decodeURIComponent($('#MaxFileSize').val());

let isAllowedTypeError = false;
let isMaxFileSizeError = false;
const formData = new FormData();

for (let file of input.files) {
const ext = file.name.slice(file.name.lastIndexOf('.') + 1).toLowerCase();
if (disallowedTypes.includes(ext)) {
isAllowedTypeError = true;
}
if (file.size * 0.000001 > maxFileSize) {
isMaxFileSizeError = true;
}
formData.append('files', file);
}

if (isAllowedTypeError) {
input.value = '';
return abp.notify.error('Error', 'File type not supported');
}
if (isMaxFileSizeError) {
input.value = '';
return abp.notify.error('Error', 'File size exceeds ' + maxFileSize + 'MB');
}

$.ajax({
url: `/api/app/attachment/email/${emailLogId}/upload`,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function () {
PubSub.publish('reload_email_attachments_table');
},
error: function (xhr) {
abp.notify.error(xhr.responseText || 'Failed to upload attachment.');
},
complete: function () {
input.value = '';
}
});
}

function deleteEmailAttachment(attachmentId) {
abp.message.confirm(
'Are you sure you want to delete this attachment?',
Expand Down
Loading
Loading