Skip to content

Commit 4eafb75

Browse files
committed
merge: SquidStd.Mail SMTP sender (IMailSender, OutgoingMailMessage, send events)
2 parents 0ad48f9 + 1dbf4cc commit 4eafb75

16 files changed

Lines changed: 511 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using SquidStd.Mail.Abstractions.Data;
2+
3+
namespace SquidStd.Mail.Abstractions.Data.Config;
4+
5+
/// <summary>Plain options for the SMTP sender (passed directly to AddMailSender).</summary>
6+
public sealed class SmtpOptions
7+
{
8+
/// <summary>SMTP server host.</summary>
9+
public string Host { get; set; } = string.Empty;
10+
11+
/// <summary>SMTP server port.</summary>
12+
public int Port { get; set; } = 587;
13+
14+
/// <summary>Use SSL-on-connect (<c>true</c>) or STARTTLS-when-available (<c>false</c>).</summary>
15+
public bool UseSsl { get; set; }
16+
17+
/// <summary>Login user name; when empty, authentication is skipped.</summary>
18+
public string Username { get; set; } = string.Empty;
19+
20+
/// <summary>Login password.</summary>
21+
public string Password { get; set; } = string.Empty;
22+
23+
/// <summary>Default sender used when a message has no <c>From</c>.</summary>
24+
public MailAddress? DefaultFrom { get; set; }
25+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using SquidStd.Core.Interfaces.Events;
2+
using SquidStd.Mail.Abstractions.Data;
3+
4+
namespace SquidStd.Mail.Abstractions.Data.Events;
5+
6+
/// <summary>Published when sending a message fails.</summary>
7+
/// <param name="To">Primary recipients.</param>
8+
/// <param name="Subject">Message subject.</param>
9+
/// <param name="Error">Failure reason.</param>
10+
public sealed record MailSendFailedEvent(IReadOnlyList<MailAddress> To, string Subject, string Error) : IEvent;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using SquidStd.Core.Interfaces.Events;
2+
using SquidStd.Mail.Abstractions.Data;
3+
4+
namespace SquidStd.Mail.Abstractions.Data.Events;
5+
6+
/// <summary>Published after a message is sent successfully.</summary>
7+
/// <param name="To">Primary recipients.</param>
8+
/// <param name="Subject">Message subject.</param>
9+
public sealed record MailSentEvent(IReadOnlyList<MailAddress> To, string Subject) : IEvent;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace SquidStd.Mail.Abstractions.Data;
2+
3+
/// <summary>An attachment to send with an outgoing message.</summary>
4+
/// <param name="FileName">Attachment file name.</param>
5+
/// <param name="ContentType">MIME content type, e.g. <c>application/pdf</c>.</param>
6+
/// <param name="Content">Raw bytes.</param>
7+
public sealed record OutgoingAttachment(string FileName, string ContentType, byte[] Content);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace SquidStd.Mail.Abstractions.Data;
2+
3+
/// <summary>An email to send. Build with an object initializer.</summary>
4+
public sealed record OutgoingMailMessage
5+
{
6+
/// <summary>Sender; falls back to <c>SmtpOptions.DefaultFrom</c> when null.</summary>
7+
public MailAddress? From { get; init; }
8+
9+
/// <summary>Primary recipients (required, non-empty).</summary>
10+
public IReadOnlyList<MailAddress> To { get; init; } = [];
11+
12+
/// <summary>Carbon-copy recipients.</summary>
13+
public IReadOnlyList<MailAddress> Cc { get; init; } = [];
14+
15+
/// <summary>Blind carbon-copy recipients.</summary>
16+
public IReadOnlyList<MailAddress> Bcc { get; init; } = [];
17+
18+
/// <summary>Subject line.</summary>
19+
public string Subject { get; init; } = string.Empty;
20+
21+
/// <summary>Plain-text body, or null.</summary>
22+
public string? TextBody { get; init; }
23+
24+
/// <summary>HTML body, or null.</summary>
25+
public string? HtmlBody { get; init; }
26+
27+
/// <summary>Attachments.</summary>
28+
public IReadOnlyList<OutgoingAttachment> Attachments { get; init; } = [];
29+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace SquidStd.Mail.Abstractions.Exceptions;
2+
3+
/// <summary>Thrown when an outbound message cannot be sent.</summary>
4+
public sealed class MailSendException : Exception
5+
{
6+
/// <summary>Initializes the exception with a message and the underlying cause.</summary>
7+
public MailSendException(string message, Exception innerException)
8+
: base(message, innerException)
9+
{
10+
}
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using SquidStd.Mail.Abstractions.Data;
2+
3+
namespace SquidStd.Mail.Abstractions.Interfaces;
4+
5+
/// <summary>Sends outbound email.</summary>
6+
public interface IMailSender
7+
{
8+
/// <summary>Sends a message; throws <see cref="Exceptions.MailSendException" /> on failure.</summary>
9+
Task SendAsync(OutgoingMailMessage message, CancellationToken cancellationToken = default);
10+
}

src/SquidStd.Mail.Abstractions/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ dotnet add package SquidStd.Mail.Abstractions
2828
| `MailReceivedEvent` | Published on the event bus per new message. |
2929
| `IMailReader` | Fetches new messages from a mailbox. |
3030
| `MailOptions` | Host/port/credentials, protocol, polling and fetch options. |
31+
| `IMailSender` | Sends outbound email. |
32+
| `OutgoingMailMessage` | An email to send (recipients, subject, bodies, attachments). |
33+
| `SmtpOptions` | SMTP host/port/SSL/credentials and a default sender. |
34+
| `MailSentEvent` / `MailSendFailedEvent` | Published on the event bus on send success/failure. |
3135

3236
## License
3337

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using DryIoc;
2+
using SquidStd.Mail.Abstractions.Data.Config;
3+
using SquidStd.Mail.Abstractions.Interfaces;
4+
using SquidStd.Mail.MailKit.Services;
5+
6+
namespace SquidStd.Mail.MailKit.Extensions;
7+
8+
/// <summary>DryIoc registration helper for the SMTP sender.</summary>
9+
public static class MailSenderRegistrationExtensions
10+
{
11+
/// <summary>Registers <see cref="SmtpOptions" /> and <see cref="IMailSender" /> (MailKit SMTP).</summary>
12+
public static IContainer AddMailSender(this IContainer container, SmtpOptions options)
13+
{
14+
ArgumentNullException.ThrowIfNull(container);
15+
ArgumentNullException.ThrowIfNull(options);
16+
ArgumentException.ThrowIfNullOrWhiteSpace(options.Host);
17+
18+
if (options.Port <= 0)
19+
{
20+
throw new ArgumentException("Port must be positive.", nameof(options));
21+
}
22+
23+
container.RegisterInstance(options);
24+
container.Register<IMailSender, MailKitMailSender>(Reuse.Singleton);
25+
26+
return container;
27+
}
28+
}

src/SquidStd.Mail.MailKit/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,36 @@ container.AddMail(new MailOptions
4444
Listen with an `IAsyncEventListener<MailReceivedEvent>` registered on the `IEventBus`. IMAP marks messages
4545
`\Seen` after fetch (configurable); POP3 dedups by UIDL and can delete after download.
4646

47+
## Sending (SMTP)
48+
49+
```csharp
50+
using DryIoc;
51+
using SquidStd.Mail.Abstractions.Data;
52+
using SquidStd.Mail.Abstractions.Data.Config;
53+
using SquidStd.Mail.Abstractions.Interfaces;
54+
using SquidStd.Mail.MailKit.Extensions;
55+
56+
container.AddMailSender(new SmtpOptions
57+
{
58+
Host = "smtp.example.com",
59+
Port = 587,
60+
UseSsl = false,
61+
Username = "user@example.com",
62+
Password = "secret",
63+
DefaultFrom = new MailAddress("App", "app@example.com")
64+
});
65+
66+
var sender = container.Resolve<IMailSender>();
67+
await sender.SendAsync(new OutgoingMailMessage
68+
{
69+
To = [new MailAddress("Bob", "bob@example.com")],
70+
Subject = "Welcome",
71+
HtmlBody = "<p>Hello!</p>"
72+
});
73+
```
74+
75+
`MailSentEvent` / `MailSendFailedEvent` are published on the `IEventBus`; failures throw `MailSendException`.
76+
4777
## License
4878

4979
MIT — part of [SquidStd](https://github.com/tgiachi/squid-std).

0 commit comments

Comments
 (0)