diff --git a/.gitignore b/.gitignore index 376092e..08865d7 100644 --- a/.gitignore +++ b/.gitignore @@ -361,4 +361,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Secrets +MicaApps.Mail.Tests/Secrets.cs diff --git a/Mail.sln b/Mail.sln index a2befa2..07605a5 100644 --- a/Mail.sln +++ b/Mail.sln @@ -24,6 +24,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicaApps.Mail.UWP", "src\Mi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicaApps.Mail", "src\MicaApps.Mail\MicaApps.Mail.csproj", "{A910A7C5-9F5C-457C-88B6-4CED1725398A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FDF4ACB0-3370-41FC-B005-221E43AC34D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicaApps.Mail.Tests", "tests\MicaApps.Mail.Tests\MicaApps.Mail.Tests.csproj", "{C1BE9EEF-C620-455E-A331-E448EB4A4615}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,12 +92,33 @@ Global {A910A7C5-9F5C-457C-88B6-4CED1725398A}.Release|x64.Build.0 = Release|Any CPU {A910A7C5-9F5C-457C-88B6-4CED1725398A}.Release|x86.ActiveCfg = Release|Any CPU {A910A7C5-9F5C-457C-88B6-4CED1725398A}.Release|x86.Build.0 = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|ARM.Build.0 = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|ARM64.Build.0 = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|x64.ActiveCfg = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|x64.Build.0 = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|x86.ActiveCfg = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Debug|x86.Build.0 = Debug|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|Any CPU.Build.0 = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|ARM.ActiveCfg = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|ARM.Build.0 = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|ARM64.ActiveCfg = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|ARM64.Build.0 = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|x64.ActiveCfg = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|x64.Build.0 = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|x86.ActiveCfg = Release|Any CPU + {C1BE9EEF-C620-455E-A331-E448EB4A4615}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {D232482A-0A17-4F0E-A242-F97C4D503937} = {9AF8B660-344F-4CDC-8EF1-CAEBE2AF4081} + {C1BE9EEF-C620-455E-A331-E448EB4A4615} = {FDF4ACB0-3370-41FC-B005-221E43AC34D0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93DE7270-2FAA-4FBE-9E2E-418D69D44180} diff --git a/src/MicaApps.Mail/Abstraction/MailService/IAttachmentGettable.cs b/src/MicaApps.Mail/Abstraction/MailService/IAttachmentGettable.cs new file mode 100644 index 0000000..8076b94 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/MailService/IAttachmentGettable.cs @@ -0,0 +1,9 @@ +using MicaApps.Mail.Abstraction.Models; +using MicaApps.Mail.Abstraction.Models.Messages; + +namespace MicaApps.Mail.Abstraction.MailService; + +public interface IAttachmentGettable +{ + public Task GetAttachmentContentAsync(MailAttachment attachment); +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/MailService/IProgressiveEmailFetchable.cs b/src/MicaApps.Mail/Abstraction/MailService/IProgressiveEmailFetchable.cs new file mode 100644 index 0000000..b84c850 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/MailService/IProgressiveEmailFetchable.cs @@ -0,0 +1,10 @@ +using MicaApps.Mail.Abstraction.Models; +using MicaApps.Mail.Abstraction.Models.Messages; + +namespace MicaApps.Mail.Abstraction.MailService; + +public interface IProgressiveEmailFetchable +{ + public Task> GetMailsInFolderAsync(MailFolder mailFolder, int start, int count, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/MailService/MailServiceBase.cs b/src/MicaApps.Mail/Abstraction/MailService/MailServiceBase.cs new file mode 100644 index 0000000..976ff92 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/MailService/MailServiceBase.cs @@ -0,0 +1,23 @@ +using MicaApps.Mail.Abstraction.Models; +using MicaApps.Mail.Abstraction.Models.Messages; + +namespace MicaApps.Mail.Abstraction.MailService; + +public abstract class MailServiceBase +{ + public abstract string Id { get; set; } + public abstract string Name { get; set; } + + public abstract Task ConnectAsync(CancellationToken cancellationToken = default); + public abstract Task DisconnectAsync(CancellationToken cancellationToken = default); + + public abstract Task> GetMailFoldersAsync(CancellationToken cancellationToken = default); + + public abstract Task> GetMailsInFolderAsync( + MailFolder mailFolder, CancellationToken cancellationToken = default); + + public abstract Task GetMailDetailAsync(string id, CancellationToken cancellationToken = default); + + + public abstract Task SendMailAsync(MailMessage sendingMailMessage, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/EmailAccount.cs b/src/MicaApps.Mail/Abstraction/Models/EmailAccount.cs new file mode 100644 index 0000000..43edcdb --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/EmailAccount.cs @@ -0,0 +1,5 @@ +namespace MicaApps.Mail.Abstraction.Models; + +public record EmailAccount(string Email, string? Name) +{ +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/MailAttachment.cs b/src/MicaApps.Mail/Abstraction/Models/MailAttachment.cs new file mode 100644 index 0000000..8dbd932 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/MailAttachment.cs @@ -0,0 +1,17 @@ +namespace MicaApps.Mail.Abstraction.Models; + +public class MailAttachment +{ + public string Id { get; } + public string? Name { get; } + public string? ContentType { get; } + public long Length { get; } + + public MailAttachment(string id,string? name,string? contentType,long length) + { + Id = id; + Name = name; + ContentType = contentType; + Length = length; + } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/MailAttachmentContent.cs b/src/MicaApps.Mail/Abstraction/Models/MailAttachmentContent.cs new file mode 100644 index 0000000..0f3d6d6 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/MailAttachmentContent.cs @@ -0,0 +1,22 @@ +namespace MicaApps.Mail.Abstraction.Models; + +public class MailAttachmentContent +{ + public MailAttachmentContent(string id, string? name, string? contentType, long length) + { + Id = id; + Name = name; + ContentType = contentType; + Length = length; + } + + public virtual Task WriteToStreamAsync(Stream stream, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public string Id { get; } + public string? Name { get; } + public string? ContentType { get; } + public long Length { get; } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/MailFolder.cs b/src/MicaApps.Mail/Abstraction/Models/MailFolder.cs new file mode 100644 index 0000000..a4f5580 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/MailFolder.cs @@ -0,0 +1,7 @@ +namespace MicaApps.Mail.Abstraction.Models; + +public class MailFolder +{ + public string Id { get; set; } = null!; + public string? Name { get; set; } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasAttachments.cs b/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasAttachments.cs new file mode 100644 index 0000000..c28e7e4 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasAttachments.cs @@ -0,0 +1,6 @@ +namespace MicaApps.Mail.Abstraction.Models.Messages.Interfaces; + +public interface IHasAttachments +{ + public List Attachments { get; set; } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasBcc.cs b/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasBcc.cs new file mode 100644 index 0000000..bd9217b --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasBcc.cs @@ -0,0 +1,6 @@ +namespace MicaApps.Mail.Abstraction.Models.Messages.Interfaces; + +public interface IHasBcc +{ + public List Bcc { get; set; } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasCc.cs b/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasCc.cs new file mode 100644 index 0000000..b45b07c --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasCc.cs @@ -0,0 +1,6 @@ +namespace MicaApps.Mail.Abstraction.Models.Messages.Interfaces; + +public interface IHasCc +{ + public List Cc { get; set; } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasHtmlBody.cs b/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasHtmlBody.cs new file mode 100644 index 0000000..c26dfc0 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/Messages/Interfaces/IHasHtmlBody.cs @@ -0,0 +1,6 @@ +namespace MicaApps.Mail.Abstraction.Models.Messages.Interfaces; + +public interface IHasHtmlBody +{ + public string? HtmlBody { get; set; } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Abstraction/Models/Messages/MailMessage.cs b/src/MicaApps.Mail/Abstraction/Models/Messages/MailMessage.cs new file mode 100644 index 0000000..ffe0185 --- /dev/null +++ b/src/MicaApps.Mail/Abstraction/Models/Messages/MailMessage.cs @@ -0,0 +1,11 @@ +namespace MicaApps.Mail.Abstraction.Models.Messages; + +public class MailMessage +{ + public string MailId { get; set; } = null!; + public string? Subject { get; set; } + public string Body { get; set; } = null!; + public DateTimeOffset SendDate { get; set; } + public EmailAccount Sender { get; set; } = null!; + public List Recipients { get; set; } = new(); +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Class1.cs b/src/MicaApps.Mail/Class1.cs deleted file mode 100644 index 16d774d..0000000 --- a/src/MicaApps.Mail/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace MicaApps.Mail; -public class Class1 -{ - -} diff --git a/src/MicaApps.Mail/Extensions/Results.cs b/src/MicaApps.Mail/Extensions/Results.cs new file mode 100644 index 0000000..234dad0 --- /dev/null +++ b/src/MicaApps.Mail/Extensions/Results.cs @@ -0,0 +1,48 @@ +namespace MicaApps.Mail.Extensions; + +public struct Results +{ + private readonly TValue? _value; + private readonly TError? _error; + + public bool IsError => !IsSuccess; + public bool IsSuccess { get; set; } + + public TValue? Value => _value; + public TError? Error => _error; + + private Results(TValue value) + { + IsSuccess = true; + _value = value; + } + + private Results(TError error) + { + IsSuccess = false; + _error = error; + } + + public static implicit operator Results(TValue value) => new(value); + public static implicit operator Results(TError error) => new(error); + + public static Results CreateError(TError error) => new(error); + public static Results CreateSuccess(TValue value) => new(value); + + /* + // 若要使用此方法, 请先去除 _value, _error 的 readonly 访问符 + public Results WithValue(TValue value) + { + _value = value; + return this; + } + + public Results WithError(TError error) + { + _error = error; + return this; + } + */ + public TResult Match(Func success, Func error) + => IsSuccess ? success(_value!) : error(_error!); +} \ No newline at end of file diff --git a/src/MicaApps.Mail/Extensions/Roslyn/System.Runtime.CompilerServices.IsExternalInit.cs b/src/MicaApps.Mail/Extensions/Roslyn/System.Runtime.CompilerServices.IsExternalInit.cs new file mode 100644 index 0000000..8a7c8f6 --- /dev/null +++ b/src/MicaApps.Mail/Extensions/Roslyn/System.Runtime.CompilerServices.IsExternalInit.cs @@ -0,0 +1,19 @@ +// +#pragma warning disable +#nullable enable annotations + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal static class IsExternalInit + { + } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/MailServices/ProtocolMailService.cs b/src/MicaApps.Mail/MailServices/ProtocolMailService.cs new file mode 100644 index 0000000..ed9146d --- /dev/null +++ b/src/MicaApps.Mail/MailServices/ProtocolMailService.cs @@ -0,0 +1,288 @@ +using System.Net; +using MailKit; +using MailKit.Net.Imap; +using MailKit.Net.Smtp; +using MailKit.Security; +using MicaApps.Mail.Abstraction.MailService; +using MicaApps.Mail.Abstraction.Models; +using MicaApps.Mail.Abstraction.Models.Messages; +using MicaApps.Mail.Abstraction.Models.Messages.Interfaces; +using MimeKit; +using MailFolder = MicaApps.Mail.Abstraction.Models.MailFolder; + +namespace MicaApps.Mail.MailServices; + +public class ProtocolMailService : MailServiceBase, IProgressiveEmailFetchable, IAttachmentGettable, IDisposable +{ + public override string Id { get; set; } = "protocol"; + public override string Name { get; set; } = "SMTP/IMAP 邮件服务"; + + private readonly SmtpClient _smtpClient = new SmtpClient(); + private readonly ImapClient _imapClient = new ImapClient(); + + public ProtocolMailSettings SmtpSettings = new ProtocolMailSettings(); + public ProtocolMailSettings ImapSettings = new ProtocolMailSettings(); + + + public override async Task ConnectAsync(CancellationToken cancellationToken = default) + { + await _smtpClient.ConnectAsync(SmtpSettings.Host, SmtpSettings.Port, (SecureSocketOptions)SmtpSettings.SecureType, + cancellationToken: cancellationToken); + await _smtpClient.AuthenticateAsync(SmtpSettings.Username, SmtpSettings.Password, + cancellationToken: cancellationToken); + + await _imapClient.ConnectAsync(ImapSettings.Host, ImapSettings.Port, (SecureSocketOptions)ImapSettings.SecureType, + cancellationToken: cancellationToken); + await _imapClient.AuthenticateAsync(ImapSettings.Username, ImapSettings.Password, + cancellationToken: cancellationToken); + } + + public override async Task DisconnectAsync(CancellationToken cancellationToken = default) + { + await _smtpClient.DisconnectAsync(true, cancellationToken); + await _smtpClient.DisconnectAsync(true, cancellationToken); + } + + public override Task> GetMailFoldersAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(new List() + { + new() + { + Id = _imapClient.Inbox.Id, + Name = _imapClient.Inbox.Name + } + }); + } + + public override async Task> GetMailsInFolderAsync( + MailFolder mailFolder, CancellationToken cancellationToken = default) + { + return await GetMailsInFolderAsync(mailFolder, 0, 50, cancellationToken); + } + + public async Task> GetMailsInFolderAsync(MailFolder mailFolder, int start, int count, + CancellationToken cancellationToken = default) + { + if (!_imapClient.Inbox.IsOpen) + await _imapClient.Inbox.OpenAsync(FolderAccess.ReadOnly, cancellationToken); + var results = (await _imapClient.Inbox.FetchAsync( + _imapClient.Inbox.Count - start - count, _imapClient.Inbox.Count - start, MessageSummaryItems.Envelope | MessageSummaryItems.PreviewText, + cancellationToken: cancellationToken)).Reverse(); + var ret = new List(); + foreach (var messageSummary in results) + { + var message = new ImapMailMessage + { + MailId = messageSummary.UniqueId.Id.ToString(), + Subject = messageSummary.Envelope.Subject, + Body = messageSummary.PreviewText, + SendDate = messageSummary.Date + }; + if (messageSummary.Envelope.Sender.FirstOrDefault() is MailboxAddress senderAddr) + { + message.Sender = new EmailAccount(senderAddr.Address, senderAddr.Name); + } + + foreach (var recipient in (messageSummary.Envelope.To.OfType())) + { + message.Recipients.Add(new EmailAccount(recipient.Address, recipient.Name)); + } + + foreach (var cc in (messageSummary.Envelope.Cc.OfType())) + { + message.Recipients.Add(new EmailAccount(cc.Address, cc.Name)); + } + + foreach (var bcc in (messageSummary.Envelope.Bcc.OfType())) + { + message.Recipients.Add(new EmailAccount(bcc.Address, bcc.Name)); + } + + if (messageSummary.Attachments.Any()) + { + foreach (var messageSummaryAttachment in messageSummary.Attachments) + { + message.Attachments.Add(new ImapMailAttachment(messageSummaryAttachment.ContentId, + messageSummaryAttachment.FileName, + messageSummaryAttachment.ContentType.MimeType, + messageSummaryAttachment.Octets, + messageSummaryAttachment, messageSummary.UniqueId)); + } + } + + ret.Add(message); + } + + return ret; + } + + public override async Task GetMailDetailAsync( + string id, CancellationToken cancellationToken = default) + { + if (!uint.TryParse(id, out var uniqueId)) + return null; + if (!_imapClient.Inbox.IsOpen) + await _imapClient.Inbox.OpenAsync(FolderAccess.ReadOnly, cancellationToken); + var messageSummary = await _imapClient.Inbox.GetMessageAsync(new UniqueId(uniqueId), cancellationToken); + + var message = new ImapMailMessage + { + MailId = id, + Subject = messageSummary.Subject, + Body = messageSummary.TextBody, + HtmlBody = messageSummary.HtmlBody, + SendDate = messageSummary.Date + }; + if (messageSummary.Sender is { } senderAddr) + { + message.Sender = new EmailAccount(senderAddr.Address, senderAddr.Name); + } + + foreach (var recipient in (messageSummary.To.OfType())) + { + message.Recipients.Add(new EmailAccount(recipient.Address, recipient.Name)); + } + + foreach (var cc in (messageSummary.Cc.OfType())) + { + message.Recipients.Add(new EmailAccount(cc.Address, cc.Name)); + } + + foreach (var bcc in (messageSummary.Bcc.OfType())) + { + message.Recipients.Add(new EmailAccount(bcc.Address, bcc.Name)); + } + + + return message; + } + + public override async Task SendMailAsync(MailMessage sendingMailMessage, + CancellationToken cancellationToken = default) + { + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(sendingMailMessage.Sender.Name, message.Sender.Address)); + foreach (var (email, name) in sendingMailMessage.Recipients) + { + message.To.Add(new MailboxAddress(name, email)); + } + + message.Subject = sendingMailMessage.Subject; + message.Body = new TextPart("plain") + { + Text = sendingMailMessage.Body + }; + + if (sendingMailMessage is IHasBcc hasBcc && hasBcc.Bcc is { Count: > 0 }) + { + foreach (var (email, name) in hasBcc.Bcc) + { + message.Bcc.Add(new MailboxAddress(name, email)); + } + } + + if (sendingMailMessage is IHasCc hasCc && hasCc.Cc is { Count: > 0 }) + { + foreach (var (email, name) in hasCc.Cc) + { + message.Cc.Add(new MailboxAddress(name, email)); + } + } + + await _smtpClient.SendAsync(message, cancellationToken); + } + + public void Dispose() + { + _smtpClient.Dispose(); + _imapClient.Dispose(); + } + + public async Task GetAttachmentContentAsync(MailAttachment attachment) + { + if (attachment is not ImapMailAttachment mailAttachment) return null; + var mime = (MimePart)await _imapClient.Inbox.GetBodyPartAsync(mailAttachment.MailId, mailAttachment.Body); + return new ImapMailAttachmentContent(mime.ContentId, mime.FileName, mime.ContentType.MimeType, + attachment.Length, mime); + } +} + +public class ProtocolMailSettings +{ + public string Host { get; set; } + public int Port { get; set; } + public SecureType SecureType { get; set; } + public string Username { get; set; } + public string Password { get; set; } +} + +public enum SecureType +{ + /// No SSL or TLS encryption should be used. + None, + + /// + /// Allow the to decide which SSL or TLS + /// options to use (default). If the server does not support SSL or TLS, + /// then the connection will continue without any encryption. + /// + Auto, + + /// + /// The connection should use SSL or TLS encryption immediately. + /// + SslOnConnect, + + /// + /// Elevates the connection to use TLS encryption immediately after + /// reading the greeting and capabilities of the server. If the server + /// does not support the STARTTLS extension, then the connection will + /// fail and a will be thrown. + /// + StartTls, + + /// + /// Elevates the connection to use TLS encryption immediately after + /// reading the greeting and capabilities of the server, but only if + /// the server supports the STARTTLS extension. + /// + StartTlsWhenAvailable, +} + +public class ImapMailMessage : MailMessage, IHasBcc, IHasCc, IHasHtmlBody, IHasAttachments +{ + public List Bcc { get; set; } = new(); + public List Cc { get; set; } = new(); + public string? HtmlBody { get; set; } + public List Attachments { get; set; } = new(); +} + +public class ImapMailAttachment : MailAttachment +{ + public BodyPartBasic Body { get; } + public UniqueId MailId { get; } + + public ImapMailAttachment(string id, string? name, string? contentType, long length, BodyPartBasic body, + UniqueId mailId) : base(id, name, contentType, length) + { + Body = body; + MailId = mailId; + } +} + +public class ImapMailAttachmentContent : MailAttachmentContent +{ + public MimePart MimePart { get; set; } + + public ImapMailAttachmentContent(string id, string? name, string? contentType, long length, MimePart mimePart) : + base(id, name, contentType, length) + { + MimePart = mimePart; + } + + public override async Task WriteToStreamAsync(Stream stream, CancellationToken cancellationToken = default) + { + await MimePart.WriteToAsync(stream, cancellationToken); + } +} \ No newline at end of file diff --git a/src/MicaApps.Mail/MicaApps.Mail.csproj b/src/MicaApps.Mail/MicaApps.Mail.csproj index 330f350..e356c90 100644 --- a/src/MicaApps.Mail/MicaApps.Mail.csproj +++ b/src/MicaApps.Mail/MicaApps.Mail.csproj @@ -5,6 +5,17 @@ latest enable enable + + + + 4.1.0 + + + + + + + diff --git a/tests/MicaApps.Mail.Tests/MicaApps.Mail.Tests.csproj b/tests/MicaApps.Mail.Tests/MicaApps.Mail.Tests.csproj new file mode 100644 index 0000000..5083153 --- /dev/null +++ b/tests/MicaApps.Mail.Tests/MicaApps.Mail.Tests.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/MicaApps.Mail.Tests/ProtocolMailServiceTests.cs b/tests/MicaApps.Mail.Tests/ProtocolMailServiceTests.cs new file mode 100644 index 0000000..4a00efb --- /dev/null +++ b/tests/MicaApps.Mail.Tests/ProtocolMailServiceTests.cs @@ -0,0 +1,47 @@ +using System.Net; +using FluentAssertions; +using MicaApps.Mail.MailServices; +using NSubstitute; + +namespace MicaApps.Mail.Tests; + + +public class ProtocolMailServiceTests : IAsyncLifetime +{ + + private readonly ProtocolMailService _mailService; + + + + public ProtocolMailServiceTests() + { + _mailService = new ProtocolMailService(); + _mailService.Name = "测试服务"; + _mailService.SmtpSettings = Secrets.SmtpSettings; + _mailService.ImapSettings = Secrets.ImapSettings; + } + + [Fact] + public async void FetchMessages() + { + // Arrange + var folders = await _mailService.GetMailFoldersAsync(); + + // Action + var mails = await _mailService.GetMailsInFolderAsync(folders[0]); + + // Assert + mails.Should().NotBeEmpty(); + } + + public async Task InitializeAsync() + { + await _mailService.ConnectAsync(); + } + + public async Task DisposeAsync() + { + await _mailService.DisconnectAsync(); + _mailService.Dispose(); + } +} \ No newline at end of file diff --git a/tests/MicaApps.Mail.Tests/Secrets.cs b/tests/MicaApps.Mail.Tests/Secrets.cs new file mode 100644 index 0000000..d8fd46a --- /dev/null +++ b/tests/MicaApps.Mail.Tests/Secrets.cs @@ -0,0 +1,28 @@ +using MicaApps.Mail.MailServices; + +namespace MicaApps.Mail.Tests; + +// THIS FILE SHOULD BE FILLED BY GITHUB ACTION +// **DO NOT** COMMIT YOUR SECRETS TO ORIGIN +public static class Secrets +{ + // TODO: Please fill this secrets file + public static ProtocolMailSettings SmtpSettings = + new() + { + Host = "", + Port = 0, + SecureType = SecureType.Auto, + Username = "", + Password = "" + }; + public static ProtocolMailSettings ImapSettings = + new() + { + Host = "", + Port = 0, + SecureType = SecureType.Auto, + Username = "", + Password = "" + }; +} \ No newline at end of file diff --git a/tests/MicaApps.Mail.Tests/Usings.cs b/tests/MicaApps.Mail.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/MicaApps.Mail.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file