mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-07 23:05:40 -04:00
Merge pull request #115 from lanedirt/105-add-email-storage-to-server-database
Make SmtpServer save emails to database
This commit is contained in:
@@ -1 +1,3 @@
|
||||
JWT_KEY=
|
||||
SMTP_ALLOWED_DOMAINS=example.tld
|
||||
SMTP_TLS_ENABLED=false
|
||||
|
||||
@@ -48,7 +48,7 @@ $ cd AliasVault
|
||||
# Make init script executable
|
||||
$ chmod +x init.sh
|
||||
|
||||
# Run the init script
|
||||
# Run the init script and follow the steps
|
||||
$ ./init.sh
|
||||
```
|
||||
|
||||
@@ -80,4 +80,5 @@ The following technologies, frameworks and libraries are used in this project:
|
||||
- [Flowbite](https://flowbite.com/) - A free and open-source UI component library based on Tailwind CSS.
|
||||
- [Konscious.Security.Cryptography](https://github.com/kmaragon/Konscious.Security.Cryptography) - A .NET library that implements Argon2id, a memory-hard password hashing algorithm.
|
||||
- [SRP.net](https://github.com/secure-remote-password/srp.net) - SRP6a Secure Remote Password protocol for secure password authentication.
|
||||
- [SqliteWasmHelper](https://github.com/JeremyLikness/SqliteWasmHelper) - The AliasVault SQLite WASM implementation is loosely based on this library.
|
||||
- [SmtpServer](https://github.com/cosullivan/SmtpServer) - A SMTP server library for .NET that is used for the virtual email address feature.
|
||||
- [MimeKit](https://github.com/jstedfast/MimeKit) - A .NET MIME creation and parser library used for the virtual email address feature.
|
||||
|
||||
@@ -31,4 +31,6 @@ services:
|
||||
ports:
|
||||
- "25:25"
|
||||
- "587:587"
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
40
init.sh
40
init.sh
@@ -49,6 +49,44 @@ populate_jwt_key() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to ask the user for SMTP_ALLOWED_DOMAINS
|
||||
set_smtp_allowed_domains() {
|
||||
if ! grep -q "^SMTP_ALLOWED_DOMAINS=" "$ENV_FILE" || [ -z "$(grep "^SMTP_ALLOWED_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "${YELLOW}Please enter the domains that should be allowed to send email, separated by commas:${NC}\n"
|
||||
read -r smtp_allowed_domains
|
||||
if grep -q "^SMTP_ALLOWED_DOMAINS=" "$ENV_FILE"; then
|
||||
awk -v domains="$smtp_allowed_domains" '/^SMTP_ALLOWED_DOMAINS=/ {$0="SMTP_ALLOWED_DOMAINS="domains} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
else
|
||||
printf "SMTP_ALLOWED_DOMAINS=${smtp_allowed_domains}\n" >> "$ENV_FILE"
|
||||
fi
|
||||
printf "${GREEN}> SMTP_ALLOWED_DOMAINS has been set in $ENV_FILE.${NC}\n"
|
||||
else
|
||||
printf "${CYAN}> SMTP_ALLOWED_DOMAINS already exists and has a value in $ENV_FILE.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to ask the user if TLS should be enabled for email
|
||||
set_smtp_tls_enabled() {
|
||||
if ! grep -q "^SMTP_TLS_ENABLED=" "$ENV_FILE" || [ -z "$(grep "^SMTP_TLS_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "${YELLOW}Do you want TLS enabled for email? (yes/no):${NC}\n"
|
||||
read -r tls_enabled
|
||||
tls_enabled=$(echo "$tls_enabled" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$tls_enabled" = "yes" ] || [ "$tls_enabled" = "y" ]; then
|
||||
tls_enabled="true"
|
||||
else
|
||||
tls_enabled="false"
|
||||
fi
|
||||
if grep -q "^SMTP_TLS_ENABLED=" "$ENV_FILE"; then
|
||||
awk -v tls="$tls_enabled" '/^SMTP_TLS_ENABLED=/ {$0="SMTP_TLS_ENABLED="tls} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
else
|
||||
printf "SMTP_TLS_ENABLED=${tls_enabled}\n" >> "$ENV_FILE"
|
||||
fi
|
||||
printf "${GREEN}> SMTP_TLS_ENABLED has been set to ${tls_enabled} in $ENV_FILE.${NC}\n"
|
||||
else
|
||||
printf "${CYAN}> SMTP_TLS_ENABLED already exists and has a value in $ENV_FILE.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to print the CLI logo
|
||||
print_logo() {
|
||||
printf "${MAGENTA}\n"
|
||||
@@ -69,6 +107,8 @@ print_logo
|
||||
printf "${BLUE}Initializing AliasVault...${NC}\n"
|
||||
create_env_file
|
||||
populate_jwt_key
|
||||
set_smtp_allowed_domains
|
||||
set_smtp_tls_enabled
|
||||
printf "${BLUE}Initialization complete.${NC}\n"
|
||||
printf "\n"
|
||||
printf "To build the images and start the containers, run the following command:\n"
|
||||
|
||||
@@ -47,6 +47,16 @@ public class AliasServerDbContext : IdentityDbContext<AliasVaultUser>
|
||||
/// </summary>
|
||||
public DbSet<Vault> Vaults { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Emails DbSet.
|
||||
/// </summary>
|
||||
public DbSet<Email> Emails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the EmailAttachments DbSet.
|
||||
/// </summary>
|
||||
public DbSet<EmailAttachment> EmailAttachments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The OnModelCreating method.
|
||||
/// </summary>
|
||||
@@ -69,19 +79,24 @@ public class AliasServerDbContext : IdentityDbContext<AliasVaultUser>
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the User - AspNetUserRefreshToken entity
|
||||
// Configure the User - AspNetUserRefreshToken entity.
|
||||
builder.Entity<AspNetUserRefreshToken>()
|
||||
.HasOne(p => p.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.UserId)
|
||||
.IsRequired();
|
||||
|
||||
// Configure the Vault - UserId entity
|
||||
// Configure the Vault - UserId entity.
|
||||
builder.Entity<Vault>()
|
||||
.HasOne(p => p.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.UserId)
|
||||
.IsRequired();
|
||||
|
||||
// Configure the Email - Attachments entity.
|
||||
builder.Entity<EmailAttachment>().HasOne(d => d.Email)
|
||||
.WithMany(p => p.Attachments)
|
||||
.HasForeignKey(d => d.EmailId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
114
src/Databases/AliasServerDb/Email.cs
Normal file
114
src/Databases/AliasServerDb/Email.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Email.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasServerDb;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an email message.
|
||||
/// </summary>
|
||||
[Index(nameof(ToLocal))]
|
||||
[Index(nameof(Date))]
|
||||
[Index(nameof(DateSystem))]
|
||||
[Index(nameof(Visible))]
|
||||
[Index(nameof(PushNotificationSent))]
|
||||
public class Email
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Email"/> class.
|
||||
/// </summary>
|
||||
public Email()
|
||||
{
|
||||
Attachments = new HashSet<EmailAttachment>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the email.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subject of the email.
|
||||
/// </summary>
|
||||
public string Subject { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sender's email address.
|
||||
/// </summary>
|
||||
public string From { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local part of the sender's email address.
|
||||
/// </summary>
|
||||
public string FromLocal { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the domain part of the sender's email address.
|
||||
/// </summary>
|
||||
public string FromDomain { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the recipient's email address.
|
||||
/// </summary>
|
||||
public string To { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local part of the recipient's email address.
|
||||
/// </summary>
|
||||
public string ToLocal { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the domain part of the recipient's email address.
|
||||
/// </summary>
|
||||
public string ToDomain { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the email was sent.
|
||||
/// </summary>
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the system date and time when the email was received.
|
||||
/// </summary>
|
||||
public DateTime DateSystem { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTML content of the email message.
|
||||
/// </summary>
|
||||
public string? MessageHtml { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the plain text content of the email message.
|
||||
/// </summary>
|
||||
public string? MessagePlain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the preview of the email message.
|
||||
/// </summary>
|
||||
public string? MessagePreview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source of the email message.
|
||||
/// </summary>
|
||||
public string MessageSource { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the email is visible.
|
||||
/// </summary>
|
||||
public bool Visible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether a push notification has been sent for the email.
|
||||
/// </summary>
|
||||
public bool PushNotificationSent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of email attachments.
|
||||
/// </summary>
|
||||
public virtual ICollection<EmailAttachment> Attachments { get; set; }
|
||||
}
|
||||
57
src/Databases/AliasServerDb/EmailAttachment.cs
Normal file
57
src/Databases/AliasServerDb/EmailAttachment.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="EmailAttachment.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasServerDb;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an email attachment.
|
||||
/// </summary>
|
||||
[Index(nameof(EmailId))]
|
||||
public class EmailAttachment
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the attachment.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bytes of the attachment.
|
||||
/// </summary>
|
||||
public byte[] Bytes { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filename of the attachment.
|
||||
/// </summary>
|
||||
public string Filename { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type of the attachment.
|
||||
/// </summary>
|
||||
public string MimeType { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filesize of the attachment.
|
||||
/// </summary>
|
||||
public int Filesize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date of the attachment.
|
||||
/// </summary>
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the email that the attachment belongs to.
|
||||
/// </summary>
|
||||
public int EmailId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email that the attachment belongs to.
|
||||
/// </summary>
|
||||
public virtual Email Email { get; set; } = null!;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
494
src/Databases/AliasServerDb/Migrations/20240720151458_AddEmailTables.Designer.cs
generated
Normal file
494
src/Databases/AliasServerDb/Migrations/20240720151458_AddEmailTables.Designer.cs
generated
Normal file
@@ -0,0 +1,494 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContext))]
|
||||
[Migration("20240718151458_AddEmailTables")]
|
||||
partial class AddEmailTables
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.6")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Verifier")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AspNetUserRefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExpireDate")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateSystem")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("From")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FromDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FromLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageHtml")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessagePlain")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessagePreview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageSource")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PushNotificationSent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("To")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.HasIndex("DateSystem");
|
||||
|
||||
b.HasIndex("PushNotificationSent");
|
||||
|
||||
b.HasIndex("ToLocal");
|
||||
|
||||
b.HasIndex("Visible");
|
||||
|
||||
b.ToTable("Emails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EmailId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Filesize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmailId");
|
||||
|
||||
b.ToTable("EmailAttachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VaultBlob")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AspNetUserRefreshToken", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.Email", "Email")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("EmailId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Email");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEmailTables : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Emails",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Subject = table.Column<string>(type: "TEXT", nullable: false),
|
||||
From = table.Column<string>(type: "TEXT", nullable: false),
|
||||
FromLocal = table.Column<string>(type: "TEXT", nullable: false),
|
||||
FromDomain = table.Column<string>(type: "TEXT", nullable: false),
|
||||
To = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ToLocal = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ToDomain = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Date = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
DateSystem = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
MessageHtml = table.Column<string>(type: "TEXT", nullable: true),
|
||||
MessagePlain = table.Column<string>(type: "TEXT", nullable: true),
|
||||
MessagePreview = table.Column<string>(type: "TEXT", nullable: true),
|
||||
MessageSource = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Visible = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
PushNotificationSent = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Emails", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "EmailAttachments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Bytes = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||
Filename = table.Column<string>(type: "TEXT", nullable: false),
|
||||
MimeType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Filesize = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Date = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
EmailId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_EmailAttachments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_EmailAttachments_Emails_EmailId",
|
||||
column: x => x.EmailId,
|
||||
principalTable: "Emails",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EmailAttachments_EmailId",
|
||||
table: "EmailAttachments",
|
||||
column: "EmailId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Emails_Date",
|
||||
table: "Emails",
|
||||
column: "Date");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Emails_DateSystem",
|
||||
table: "Emails",
|
||||
column: "DateSystem");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Emails_PushNotificationSent",
|
||||
table: "Emails",
|
||||
column: "PushNotificationSent");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Emails_ToLocal",
|
||||
table: "Emails",
|
||||
column: "ToLocal");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Emails_Visible",
|
||||
table: "Emails",
|
||||
column: "Visible");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "EmailAttachments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Emails");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,114 @@ namespace AliasServerDb.Migrations
|
||||
b.ToTable("AspNetUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateSystem")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("From")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FromDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FromLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageHtml")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessagePlain")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessagePreview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageSource")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PushNotificationSent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("To")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.HasIndex("DateSystem");
|
||||
|
||||
b.HasIndex("PushNotificationSent");
|
||||
|
||||
b.HasIndex("ToLocal");
|
||||
|
||||
b.HasIndex("Visible");
|
||||
|
||||
b.ToTable("Emails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EmailId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Filesize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmailId");
|
||||
|
||||
b.ToTable("EmailAttachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -300,6 +408,17 @@ namespace AliasServerDb.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.Email", "Email")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("EmailId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Email");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
@@ -361,6 +480,11 @@ namespace AliasServerDb.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
|
||||
<PackageReference Include="MimeKit" Version="4.7.1" />
|
||||
<PackageReference Include="NUglify" Version="1.21.9" />
|
||||
<PackageReference Include="SmtpServer" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
45
src/Services/AliasVault.SmtpService/AllowedDomainsFilter.cs
Normal file
45
src/Services/AliasVault.SmtpService/AllowedDomainsFilter.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="AllowedDomainsFilter.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.SmtpService;
|
||||
|
||||
using SmtpServer;
|
||||
using SmtpServer.Mail;
|
||||
using SmtpServer.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Filter to allow only emails from configured domains.
|
||||
/// </summary>
|
||||
public class AllowedDomainsFilter(Config config, ILogger<AllowedDomainsFilter> logger) : IMailboxFilter, IMailboxFilterFactory
|
||||
{
|
||||
private readonly TimeSpan _delay = TimeSpan.Zero;
|
||||
|
||||
public async Task<bool> CanAcceptFromAsync(ISessionContext context, IMailbox from, int size, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(_delay, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> CanDeliverToAsync(ISessionContext context, IMailbox to, IMailbox from, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(_delay, cancellationToken);
|
||||
|
||||
if (!config.AllowedToDomains.Contains(to.Host.ToLowerInvariant()))
|
||||
{
|
||||
// ToAddress host is not allowed, return error to sender.
|
||||
logger.LogWarning("Email to {ToAddress} is not allowed", to);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IMailboxFilter CreateInstance(ISessionContext context)
|
||||
{
|
||||
return new AllowedDomainsFilter(context.ServiceProvider.GetRequiredService<Config>(), context.ServiceProvider.GetRequiredService<ILogger<AllowedDomainsFilter>>());
|
||||
}
|
||||
}
|
||||
323
src/Services/AliasVault.SmtpService/DatabaseMessageStore.cs
Normal file
323
src/Services/AliasVault.SmtpService/DatabaseMessageStore.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="DatabaseMessageStore.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using System.Buffers;
|
||||
using System.Net.Mail;
|
||||
using System.Text.RegularExpressions;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MimeKit;
|
||||
using NUglify;
|
||||
using SmtpServer;
|
||||
using SmtpServer.Protocol;
|
||||
using SmtpServer.Storage;
|
||||
|
||||
namespace AliasVault.SmtpService;
|
||||
|
||||
/// <summary>
|
||||
/// Custom exception for when the email parsing fails to find the "to" address in the email.
|
||||
/// </summary>
|
||||
public class EmailParseMissingToException(string message) : Exception(message);
|
||||
|
||||
/// <summary>
|
||||
/// Database message store.
|
||||
/// </summary>
|
||||
/// <param name="logger">ILogger instance.</param>
|
||||
/// <param name="config">Config instance.</param>
|
||||
public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config config, IDbContextFactory<AliasServerDbContext> dbContextFactory) : MessageStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Override the SaveAsync method to save the email into the database.
|
||||
/// </summary>
|
||||
/// <param name="context">ISessionContext instance.</param>
|
||||
/// <param name="transaction">IMessageTransaction instance.</param>
|
||||
/// <param name="buffer">Buffer which contains the email contents.</param>
|
||||
/// <param name="cancellationToken">CancellationToken instance.</param>
|
||||
/// <returns>SmtpResponse.</returns>
|
||||
public override async Task<SmtpResponse> SaveAsync(ISessionContext context, IMessageTransaction transaction, ReadOnlySequence<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = new MemoryStream();
|
||||
|
||||
var position = buffer.GetPosition(0);
|
||||
while (buffer.TryGet(ref position, out var memory))
|
||||
{
|
||||
stream.Write(memory.Span);
|
||||
}
|
||||
|
||||
// Max email filesize limit: 10MB. If the mail is larger in size, reject it.
|
||||
// Because of base64 encoding which has approx 33% increase in binary size
|
||||
// we multiply the limit by 1.4 to be safe.
|
||||
var maxEmailSizeInMegabytes = 10;
|
||||
if (stream.Length > ((maxEmailSizeInMegabytes * 1024 * 1024) * 1.4))
|
||||
{
|
||||
return SmtpResponse.SizeLimitExceeded;
|
||||
}
|
||||
|
||||
stream.Position = 0;
|
||||
var message = await MimeKit.MimeMessage.LoadAsync(stream, cancellationToken);
|
||||
// Retrieve all addresses from the SMTP transaction which should contain all recipients for this mail instance.
|
||||
var allAddresses = transaction.To
|
||||
.Distinct()
|
||||
.ToList();
|
||||
// Limit list to 15 addresses max. (to prevent mailbomb spam abuse)
|
||||
var toAddresses = allAddresses.Take(15).ToList();
|
||||
// For every toAddress
|
||||
foreach (var toAddress in toAddresses)
|
||||
{
|
||||
if (toAddress == null)
|
||||
{
|
||||
// No toAddress, skip.
|
||||
logger.LogWarning("Skip email, no toAddress available");
|
||||
return SmtpResponse.NoValidRecipientsGiven;
|
||||
}
|
||||
if (!config.AllowedToDomains.Contains(toAddress.Host.ToLowerInvariant()))
|
||||
{
|
||||
// ToAddress domain is not allowed, return error to sender.
|
||||
logger.LogWarning("Email to {ToAddress} is not allowed", toAddress.User + "@" + toAddress.Host);
|
||||
return SmtpResponse.NoValidRecipientsGiven;
|
||||
}
|
||||
|
||||
// Remove existing x-receiver and x-sender headers to avoid duplication.
|
||||
message.Headers.RemoveAll("x-receiver");
|
||||
message.Headers.RemoveAll("x-sender");
|
||||
|
||||
// Add new x-receiver and x-sender headers.
|
||||
message.Headers.Add("x-receiver", toAddress.User + "@" + toAddress.Host);
|
||||
message.Headers.Add("x-sender", transaction.From.User + "@" + transaction.From.Host);
|
||||
|
||||
var insertedId = await InsertEmailIntoDatabase(message);
|
||||
logger.LogInformation("Email saved into database with ID {insertedId}.", insertedId);
|
||||
}
|
||||
|
||||
return SmtpResponse.Ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insert email into database.
|
||||
/// </summary>
|
||||
/// <param name="message">MimeMessage to save into database.</param>
|
||||
private async Task<int> InsertEmailIntoDatabase(MimeMessage message)
|
||||
{
|
||||
var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var newEmail = ConvertMimeMessageToEmail(message);
|
||||
|
||||
await dbContext.Emails.AddAsync(newEmail);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return newEmail.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert MimeMessage to Email database object.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="EmailParseMissingToException"></exception>
|
||||
private static Email ConvertMimeMessageToEmail(MimeMessage message)
|
||||
{
|
||||
string from = "";
|
||||
|
||||
try
|
||||
{
|
||||
from = message.From.FirstOrDefault()?.ToString() ?? string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
string fromLocal;
|
||||
string fromDomain;
|
||||
// Try to extract from address firstly from "from" in the mail.
|
||||
try
|
||||
{
|
||||
MailAddress fromAddress = new MailAddress(message.From.FirstOrDefault()?.ToString() ?? string.Empty);
|
||||
fromLocal = fromAddress.User;
|
||||
fromDomain = fromAddress.Host;
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the above fails, try to find the x-sender in the mail
|
||||
try
|
||||
{
|
||||
MailAddress fromAddress = new MailAddress(message.Headers.First(x => x.Field == "x-sender").Value.ToString());
|
||||
fromLocal = fromAddress.User;
|
||||
fromDomain = fromAddress.Host;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If this fails as well, then simply use a blank value
|
||||
fromLocal = "";
|
||||
fromDomain = "";
|
||||
}
|
||||
}
|
||||
|
||||
MailAddress toAddress;
|
||||
string to;
|
||||
|
||||
// Try to extract to address firstly from x-receiver address..
|
||||
try
|
||||
{
|
||||
to = message.Headers.First(x => x.Field == "x-receiver").Value.ToString();
|
||||
toAddress = new MailAddress(to);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the above fails, try to find the "to" in the mail
|
||||
try
|
||||
{
|
||||
to = message.To.FirstOrDefault()?.ToString() ?? "";
|
||||
toAddress = new MailAddress(to);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If this fails as well, then simply let it throw an error to the caller.
|
||||
throw new EmailParseMissingToException("Could not find x-receiver or to address in email.");
|
||||
}
|
||||
}
|
||||
|
||||
// Create email object
|
||||
var email = new Email();
|
||||
email.From = from;
|
||||
email.FromLocal = fromLocal;
|
||||
email.FromDomain = fromDomain;
|
||||
|
||||
email.To = to;
|
||||
// Local part to lowercase, as mailboxes are always lowercase
|
||||
email.ToLocal = toAddress.User.ToLower();
|
||||
email.ToDomain = toAddress.Host;
|
||||
|
||||
email.Subject = message.Subject ?? "";
|
||||
email.MessageHtml = message.HtmlBody;
|
||||
email.MessagePlain = message.TextBody;
|
||||
email.MessageSource = message.ToString();
|
||||
|
||||
// Extract message preview text based on body contents.
|
||||
email.MessagePreview = ExtractMessagePreview(email);
|
||||
|
||||
email.Date = message.Date.DateTime;
|
||||
email.DateSystem = DateTime.UtcNow;
|
||||
email.Visible = true;
|
||||
|
||||
// Parse attachments from email, and create separate attachment records in database for each attachment
|
||||
foreach (var attachment in message.Attachments)
|
||||
{
|
||||
var emailAttachment = CreateEmailAttachment(attachment);
|
||||
email.Attachments.Add(emailAttachment);
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a preview of the email message body to be used in the email listing preview in the UI.
|
||||
/// This so the client does not need to load the full email body.
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
/// <returns></returns>
|
||||
private static string ExtractMessagePreview(Email email)
|
||||
{
|
||||
var messagePreview = string.Empty;
|
||||
const int maxPreviewLength = 180;
|
||||
|
||||
try
|
||||
{
|
||||
if (email.MessagePlain != null && !String.IsNullOrEmpty(email.MessagePlain) && email.MessagePlain.Length > 3)
|
||||
{
|
||||
// Replace any newline characters with a space
|
||||
string plainToPlainText = Regex.Replace(email.MessagePlain, @"\t|\n|\r", " ", RegexOptions.NonBacktracking);
|
||||
|
||||
// Remove all "-" or "=" characters if there are 3 or more in a row
|
||||
plainToPlainText = Regex.Replace(plainToPlainText, @"-{3,}|\={3,}", "", RegexOptions.NonBacktracking);
|
||||
|
||||
// Remove any non-printable characters
|
||||
plainToPlainText = Regex.Replace(plainToPlainText, @"[^\u0020-\u007E]", "", RegexOptions.NonBacktracking);
|
||||
|
||||
// Replace multiple spaces with a single space
|
||||
plainToPlainText = Regex.Replace(plainToPlainText, @"\s+", " ", RegexOptions.NonBacktracking);
|
||||
|
||||
// Trim start and end of string
|
||||
plainToPlainText = plainToPlainText.Trim();
|
||||
|
||||
messagePreview = plainToPlainText.Length > maxPreviewLength
|
||||
? plainToPlainText.Substring(0, maxPreviewLength)
|
||||
: plainToPlainText;
|
||||
}
|
||||
else if (email.MessageHtml != null)
|
||||
{
|
||||
string htmlToPlainText = Uglify.HtmlToText(email.MessageHtml).ToString();
|
||||
|
||||
// Replace any newline characters with a space
|
||||
htmlToPlainText = Regex.Replace(htmlToPlainText, @"\t|\n|\r", " ", RegexOptions.NonBacktracking);
|
||||
|
||||
// Remove all "-" or "=" characters if there are 3 or more in a row
|
||||
htmlToPlainText = Regex.Replace(htmlToPlainText, @"-{3,}|\={3,}", "", RegexOptions.NonBacktracking);
|
||||
|
||||
// Remove any non-printable characters
|
||||
htmlToPlainText = Regex.Replace(htmlToPlainText, @"[^\u0020-\u007E]", "", RegexOptions.NonBacktracking);
|
||||
|
||||
// Replace multiple spaces with a single space
|
||||
htmlToPlainText = Regex.Replace(htmlToPlainText, @"\s+", " ", RegexOptions.NonBacktracking);
|
||||
|
||||
// Trim start and end of string
|
||||
htmlToPlainText = htmlToPlainText.Trim();
|
||||
|
||||
messagePreview =
|
||||
htmlToPlainText.Length > maxPreviewLength ? htmlToPlainText.Substring(0, maxPreviewLength) : htmlToPlainText;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Extracting useful words from email failed.. Skip the step, do nothing..
|
||||
}
|
||||
|
||||
return messagePreview;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an EmailAttachment object from a MimeEntity attachment.
|
||||
/// </summary>
|
||||
/// <param name="attachment"></param>
|
||||
/// <returns></returns>
|
||||
private static EmailAttachment CreateEmailAttachment(MimeEntity attachment)
|
||||
{
|
||||
byte[] fileBytes = GetAttachmentBytes(attachment);
|
||||
|
||||
return new EmailAttachment
|
||||
{
|
||||
Bytes = fileBytes,
|
||||
Filename = attachment.ContentDisposition?.FileName ?? "",
|
||||
MimeType = attachment.ContentType.MimeType,
|
||||
Filesize = fileBytes.Length,
|
||||
Date = DateTime.Now
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the attachment bytes from a MimeEntity attachment.
|
||||
/// </summary>
|
||||
/// <param name="attachment"></param>
|
||||
/// <returns></returns>
|
||||
private static byte[] GetAttachmentBytes(MimeEntity attachment)
|
||||
{
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
if (attachment is MimePart mimePartAttachment)
|
||||
{
|
||||
mimePartAttachment.Content.DecodeTo(memory);
|
||||
}
|
||||
else
|
||||
{
|
||||
((MessagePart)attachment).Message.WriteTo(memory);
|
||||
}
|
||||
|
||||
return memory.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,50 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using System.Data.Common;
|
||||
using AliasVault.SmtpService;
|
||||
using SmtpServer;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using AliasServerDb;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SmtpServer.Storage;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
|
||||
// Read settings from appsettings.json.
|
||||
ConfigurationManager configuration = builder.Configuration;
|
||||
Config config = configuration.GetSection("Config").Get<Config>()!;
|
||||
// Create global config object, get values from environment variables.
|
||||
Config config = new Config();
|
||||
var emailDomains = Environment.GetEnvironmentVariable("SMTP_ALLOWED_DOMAINS")
|
||||
?? throw new KeyNotFoundException("SMTP_ALLOWED_DOMAINS environment variable is not set.");
|
||||
config.AllowedToDomains = emailDomains.Split(',').ToList();
|
||||
|
||||
var tlsEnabled = Environment.GetEnvironmentVariable("SMTP_TLS_ENABLED")
|
||||
?? throw new KeyNotFoundException("SMTP_TLS_ENABLED environment variable is not set.");
|
||||
config.SmtpTlsEnabled = tlsEnabled;
|
||||
builder.Services.AddSingleton(config);
|
||||
|
||||
builder.Services.AddSingleton<DbConnection>(container =>
|
||||
{
|
||||
var configFile = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
var connection = new SqliteConnection(configFile.GetConnectionString("AliasServerDbContext"));
|
||||
connection.Open();
|
||||
|
||||
return connection;
|
||||
});
|
||||
|
||||
builder.Services.AddDbContextFactory<AliasServerDbContext>((container, options) =>
|
||||
{
|
||||
var connection = container.GetRequiredService<DbConnection>();
|
||||
options.UseSqlite(connection).UseLazyLoadingProxies();
|
||||
});
|
||||
|
||||
builder.Services.AddTransient<IMessageStore, DatabaseMessageStore>();
|
||||
builder.Services.AddTransient<IMailboxFilter, AllowedDomainsFilter>();
|
||||
|
||||
builder.Services.AddSingleton(
|
||||
provider =>
|
||||
{
|
||||
@@ -26,16 +58,16 @@ builder.Services.AddSingleton(
|
||||
if (config.SmtpTlsEnabled == "true")
|
||||
{
|
||||
// With TLS and certificate support.
|
||||
options.Endpoint(builder =>
|
||||
builder
|
||||
options.Endpoint(serverBuilder =>
|
||||
serverBuilder
|
||||
.Port(25, false)
|
||||
.AllowUnsecureAuthentication(true)
|
||||
.AllowUnsecureAuthentication()
|
||||
.Certificate(CreateCertificate())
|
||||
.SupportedSslProtocols(System.Security.Authentication.SslProtocols.Tls12))
|
||||
.Endpoint(builder =>
|
||||
builder
|
||||
.Endpoint(serverBuilder =>
|
||||
serverBuilder
|
||||
.Port(587, false)
|
||||
.AllowUnsecureAuthentication(true)
|
||||
.AllowUnsecureAuthentication()
|
||||
.Certificate(CreateCertificate())
|
||||
.SupportedSslProtocols(System.Security.Authentication.SslProtocols.Tls12)
|
||||
);
|
||||
@@ -43,18 +75,17 @@ builder.Services.AddSingleton(
|
||||
else
|
||||
{
|
||||
// No TLS
|
||||
options.Endpoint(builder =>
|
||||
builder
|
||||
options.Endpoint(serverBuilder =>
|
||||
serverBuilder
|
||||
.Port(25, false))
|
||||
.Endpoint(builder =>
|
||||
builder
|
||||
.Endpoint(serverBuilder =>
|
||||
serverBuilder
|
||||
.Port(587, false)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to create an X509Certificate2 object from a PEM file.
|
||||
/// </summary>
|
||||
return new SmtpServer.SmtpServer(options.Build(), provider.GetRequiredService<IServiceProvider>());
|
||||
|
||||
static X509Certificate2 CreateCertificate()
|
||||
{
|
||||
// Specify the directory where PEM files are stored.
|
||||
@@ -79,16 +110,16 @@ builder.Services.AddSingleton(
|
||||
// NOTE: this is important because saving the object to a PFX file to disk for a brief
|
||||
// second will allow Windows to correctly load the certificate with the private key.
|
||||
// If we don't do this, the certificate will be loaded without the private key and
|
||||
// will throw error on Windows:
|
||||
// will throw error on Windows:
|
||||
// "The TLS server credential's certificate does not have a private key information property attached to it"
|
||||
cert = new X509Certificate2(cert.Export(X509ContentType.Pfx));
|
||||
|
||||
return cert;
|
||||
}
|
||||
|
||||
return new SmtpServer.SmtpServer(options.Build(), provider.GetRequiredService<IServiceProvider>());
|
||||
}
|
||||
);
|
||||
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
|
||||
var host = builder.Build();
|
||||
await host.RunAsync();
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"AliasVault.SmtpService": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"SMTP_ALLOWED_DOMAINS": "example.tld",
|
||||
"SMTP_TLS_ENABLED": "false"
|
||||
},
|
||||
"dotnetRunMessages": true
|
||||
},
|
||||
@@ -12,4 +14,4 @@
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json"
|
||||
}
|
||||
}
|
||||
|
||||
1
src/Services/AliasVault.SmtpService/Scripts/sendEmailAllowed.sh
Executable file
1
src/Services/AliasVault.SmtpService/Scripts/sendEmailAllowed.sh
Executable file
@@ -0,0 +1 @@
|
||||
curl --url "smtp://localhost:25" --mail-from "sender@example.com" --mail-rcpt "yourname@example.tld" --upload-file testEmail1.txt
|
||||
1
src/Services/AliasVault.SmtpService/Scripts/sendEmailNotAllowed.sh
Executable file
1
src/Services/AliasVault.SmtpService/Scripts/sendEmailNotAllowed.sh
Executable file
@@ -0,0 +1 @@
|
||||
curl --url "smtp://localhost:25" --mail-from "sender@example.com" --mail-rcpt "yourname@unknowndomain.com" --upload-file testEmail1.txt
|
||||
@@ -0,0 +1,5 @@
|
||||
From: sender@example.com
|
||||
To: recipient@example.tld
|
||||
Subject: Test Email
|
||||
|
||||
This is a test email.
|
||||
@@ -10,13 +10,13 @@ namespace AliasVault.SmtpService
|
||||
public class Worker(ILogger<Worker> logger, SmtpServer.SmtpServer smtpServer) : BackgroundService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
logger.LogInformation("AliasVault.SmtpService running at: {Time}", DateTimeOffset.Now);
|
||||
}
|
||||
await smtpServer.StartAsync(stoppingToken);
|
||||
return smtpServer.StartAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"Config": {
|
||||
"SmtpTlsEnabled": "false",
|
||||
"AllowedToDomains": [
|
||||
"example.tld"
|
||||
]
|
||||
"ConnectionStrings": {
|
||||
"AliasServerDbContext": "Data Source=../../../database/AliasServerDb.sqlite"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user