//-----------------------------------------------------------------------
//
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
//
//-----------------------------------------------------------------------
namespace AliasVault.Api.Controllers.Email;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using AliasVault.Shared.Models.Spamok;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
///
/// Email controller for retrieving emails from the database.
///
/// ILogger instance.
/// DbContext instance.
/// UserManager instance.
[ApiVersion("1")]
public class EmailController(ILogger logger, IAliasServerDbContextFactory dbContextFactory, UserManager userManager) : AuthenticatedRequestController(userManager)
{
///
/// Get the email with the specified ID.
///
/// The email ID to open.
/// List of aliases in JSON format.
[HttpGet(template: "{id}", Name = "GetEmail")]
public async Task GetEmail(int id)
{
await using var context = await dbContextFactory.CreateDbContextAsync();
var (email, errorResult) = await AuthenticateAndRetrieveEmailAsync(id, context);
if (errorResult != null)
{
return errorResult;
}
var returnEmail = new EmailApiModel
{
Id = email!.Id,
Subject = email.Subject,
FromDisplay = email.From,
FromDomain = email.FromDomain,
FromLocal = email.FromLocal,
ToDomain = email.ToDomain,
ToLocal = email.ToLocal,
Date = email.Date,
DateSystem = DateTime.SpecifyKind(email.DateSystem, DateTimeKind.Utc),
SecondsAgo = (int)DateTime.UtcNow.Subtract(email.DateSystem).TotalSeconds,
MessageHtml = email.MessageHtml,
MessagePlain = email.MessagePlain,
EncryptedSymmetricKey = email.EncryptedSymmetricKey,
EncryptionKey = email.EncryptionKey.PublicKey,
};
// Add attachment metadata (without the filebytes)
var attachments = await context.EmailAttachments.Where(x => x.EmailId == email.Id).Select(x => new AttachmentApiModel()
{
Id = x.Id,
Email_Id = x.EmailId,
Filename = x.Filename,
MimeType = x.MimeType,
Filesize = x.Filesize,
}).ToListAsync();
returnEmail.Attachments = attachments;
return Ok(returnEmail);
}
///
/// Deletes an email for the current user.
///
/// The email ID to delete.
/// A response indicating the success or failure of the deletion.
[HttpDelete(template: "{id}", Name = "DeleteEmail")]
public async Task DeleteEmail(int id)
{
await using var context = await dbContextFactory.CreateDbContextAsync();
var (email, errorResult) = await AuthenticateAndRetrieveEmailAsync(id, context);
if (errorResult != null)
{
return errorResult;
}
// Delete the email - attachments will be cascade deleted
context.Emails.Remove(email!);
try
{
await context.SaveChangesAsync();
return Ok();
}
catch (Exception ex)
{
// Log the exception
logger.LogError(ex, "An error occurred while deleting email with ID {id}.", id);
return StatusCode(500, $"An error occurred while deleting the email: {ex.Message}");
}
}
///
/// Get the attachment bytes for the specified email and attachment ID.
///
/// The email ID.
/// The attachment ID.
/// Attachment bytes in encrypted form.
[HttpGet(template: "{id}/attachments/{attachmentId}", Name = "GetEmailAttachment")]
public async Task GetEmailAttachment(int id, int attachmentId)
{
await using var context = await dbContextFactory.CreateDbContextAsync();
var (email, errorResult) = await AuthenticateAndRetrieveEmailAsync(id, context);
if (errorResult != null)
{
return errorResult;
}
// Find the requested attachment
var attachment = await context.EmailAttachments
.FirstOrDefaultAsync(x => x.Id == attachmentId && x.EmailId == email!.Id);
if (attachment == null)
{
return NotFound("Attachment not found.");
}
// Return the encrypted bytes
return File(attachment.Bytes, attachment.MimeType, attachment.Filename);
}
///
/// Authenticates the user and retrieves the requested email.
///
/// The email ID to retrieve.
/// The database context.
/// A tuple containing the authenticated user, the email, and an IActionResult if there's an error.
private async Task<(Email? Email, IActionResult? ErrorResult)> AuthenticateAndRetrieveEmailAsync(int id, AliasServerDbContext context)
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return (null, Unauthorized("Not authenticated."));
}
// Retrieve email from database.
var email = await context.Emails
.Include(x => x.Attachments)
.FirstOrDefaultAsync(x => x.Id == id);
if (email is null)
{
return (null, NotFound("Email not found."));
}
// See if this user has a valid claim to the email address.
var normalizedEmailAddress = email.To.Trim().ToLower();
var emailClaim = await context.UserEmailClaims.FirstOrDefaultAsync(x => x.UserId == user.Id && x.Address == normalizedEmailAddress);
if (emailClaim is null)
{
return (null, Unauthorized("User does not have a claim to this email address."));
}
return (email, null);
}
}