Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# If this file is renamed, the incrementing run attempt number will be reset.

name: CI

on:
push:
branches: [ "dev", "main" ]
pull_request:
branches: [ "dev", "main" ]

env:
CI_BUILD_NUMBER_BASE: ${{ github.run_number }}
CI_TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}

jobs:
build:

runs-on: ubuntu-24.04

permissions:
contents: write

steps:
- uses: actions/checkout@v4
- name: Setup
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Compute build number
run: |
echo "CI_BUILD_NUMBER=$(($CI_BUILD_NUMBER_BASE+90))" >> $GITHUB_ENV
- name: Build and Publish
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: pwsh
run: |
./Build.ps1
82 changes: 46 additions & 36 deletions Build.ps1
Original file line number Diff line number Diff line change
@@ -1,68 +1,78 @@
# This script originally (c) 2016 Serilog Contributors - license Apache 2.0

echo "build: Build started"
Write-Output "build: Build started"

Push-Location $PSScriptRoot

Write-Output "build: Tool versions follow"

dotnet --version
dotnet --list-sdks

if(Test-Path .\artifacts) {
echo "build: Cleaning .\artifacts"
Remove-Item .\artifacts -Force -Recurse
Write-Output "build: Cleaning ./artifacts"
Remove-Item ./artifacts -Force -Recurse
}

& dotnet restore --no-cache
if($LASTEXITCODE -ne 0) { exit 1 }

$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL];
$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"]
$dbp = [Xml] (Get-Content .\Directory.Build.props)
$versionPrefix = $dbp.Project.PropertyGroup.VersionPrefix

echo "build: Version suffix is $suffix"
Write-Output "build: Package version prefix is $versionPrefix"

foreach ($src in ls src/Seq.App.*) {
$branch = @{ $true = $env:CI_TARGET_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:CI_TARGET_BRANCH];
$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:CI_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:CI_BUILD_NUMBER];
$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)) -replace '([^a-zA-Z0-9\-]*)', '')-$revision"}[$branch -eq "main" -and $revision -ne "local"]

Write-Output "build: Package version suffix is $suffix"

foreach ($src in Get-ChildItem src/*) {
Push-Location $src

echo "build: Packaging app project in $src"
Write-Output "build: Packaging project in $src"

if (Test-Path ./obj/publish) {
Remove-Item -Recurse -Force ./obj/publish
}

if ($suffix) {
& dotnet publish -c Release -o ./obj/publish --version-suffix=$suffix
& dotnet pack -c Release -o ..\..\artifacts --no-build --version-suffix=$suffix
& dotnet pack -c Release -o ../../artifacts --no-build --version-suffix=$suffix
} else {
& dotnet publish -c Release -o ./obj/publish
& dotnet pack -c Release -o ..\..\artifacts --no-build
& dotnet pack -c Release -o ../../artifacts --no-build
}
if($LASTEXITCODE -ne 0) { throw "Build failed" }
if($LASTEXITCODE -ne 0) { throw "Packaging failed" }

Pop-Location
}

foreach ($src in @("src/Seq.Syntax", "src/Seq.Mail", "src/Seq.Apps.Testing")) {
Push-Location $src

echo "build: Packaging library in $src"

if ($suffix) {
& dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix
} else {
& dotnet pack -c Release -o ..\..\artifacts
}
if($LASTEXITCODE -ne 0) { throw "Build failed" }

Pop-Location
}
Write-Output "build: Checking complete solution builds"
& dotnet build
if($LASTEXITCODE -ne 0) { throw "Solution build failed" }

foreach ($test in ls test/*.Tests) {
foreach ($test in Get-ChildItem test/*.Tests) {
Push-Location $test

echo "build: Testing project in $test"
Write-Output "build: Testing project in $test"

& dotnet test -c Release
if($LASTEXITCODE -ne 0) { throw "Testing failed" }

Pop-Location
}

Pop-Location
Pop-Location

if ($env:NUGET_API_KEY) {
# GitHub Actions will only supply this to branch builds and not PRs. We publish
# builds from any branch this action targets (i.e. main and dev).

Write-Output "build: Publishing NuGet packages"

foreach ($nupkg in Get-ChildItem artifacts/*.nupkg) {
& dotnet nuget push -k $env:NUGET_API_KEY -s https://api.nuget.org/v3/index.json "$nupkg"
if($LASTEXITCODE -ne 0) { throw "Publishing failed" }
}

if (!($suffix)) {
Write-Output "build: Creating release for version $versionPrefix"

iex "gh release create v$versionPrefix --title v$versionPrefix --generate-notes $(get-item ./artifacts/*.nupkg) $(get-item ./artifacts/*.snupkg)"
}
}
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.1.0</VersionPrefix>
</PropertyGroup>
</Project>
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Seq Mail Apps [![Build status](https://ci.appveyor.com/api/projects/status/6jo5xhyfans07msl/branch/dev?svg=true)](https://ci.appveyor.com/project/datalust/seq-app-mail/branch/dev)
# Seq Mail Apps

This repository contains the Seq output apps for various email services, built on a shared email templating system.

| Package id | Description |
|-------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`Seq.App.Mail.AmazonSes`](https://nuget.org/packages/seq.app.mail.amazonses) | Send email using the Amazon Simple Email Service (SES) API. |
| [`Seq.App.Mail.Microsoft365`](https://nuget.org/packages/seq.app.mail.microsoft365) | Send email through Microsoft 365 using the Microsoft Graph API. |
| [`Seq.App.Mail.Smtp`](https://nuget.org/packages/seq.app.mail.smtp) | Send email using the SMTP protocol. |

> Need to send email using a protocol or API not listed here? Let us know!


## Getting started

Install the app under _Settings > Apps_, using one of the package ids from the table above.
Expand All @@ -30,9 +30,16 @@ When starting an instance of the app in Seq, the following parameters can be sup
| **Body** | The email body. | Yes | See `src/Seq.Mail/Resources` |
| **Body is plain text** | If checked, the body template will be interpreted as plain text, rather than HTML. | | |

### `Seq.App.Mail.AmazonSes`

| Property | Description | Template? | Default |
|-------------------|------------------------------|---|---|
| **Access key id** | An Amazon SES access key id. | | |
| **Client id** | An Amazon SES secret key. | | |

### `Seq.App.Mail.Microsoft365`

To send mail using the Microsoft 365 app, first create an app registration in Azure. The app must have the `Mail.Send` permission
To send mail using the Microsoft 365 app, first create an app registration in Azure. The app must have the `Mail.Send` permission
for the Microsoft Graph API.

| Property | Description | Template? | Default |
Expand All @@ -54,6 +61,8 @@ for the Microsoft Graph API.

## Templates

The Seq mail apps support the [Seq template language](https://docs.datalust.co/docs/template-syntax).

Event and notification properties can be inserted dynamically into many of the settings listed above, by surrounding them
with braces:

Expand Down
23 changes: 0 additions & 23 deletions appveyor.yml

This file was deleted.

2 changes: 1 addition & 1 deletion harness/Seq.Mail.TestHarness/Seq.Mail.TestHarness.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
19 changes: 17 additions & 2 deletions seq-app-mail.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sln", "sln", "{0F8E84A5-952
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
appveyor.yml = appveyor.yml
Build.ps1 = Build.ps1
LICENSE = LICENSE
README.md = README.md
RunLocalSmtp.ps1 = RunLocalSmtp.ps1
Directory.Build.props = Directory.Build.props
Build.ps1 = Build.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "asset", "asset", "{AC60371C-2888-45BB-9770-2A690DEA80FA}"
Expand Down Expand Up @@ -43,6 +43,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "harness", "harness", "{E684
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seq.Mail.TestHarness", "harness\Seq.Mail.TestHarness\Seq.Mail.TestHarness.csproj", "{53B48FC5-E76D-4442-9259-DB57CDB53232}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seq.App.Mail.AmazonSes", "src\Seq.App.Mail.AmazonSes\Seq.App.Mail.AmazonSes.csproj", "{D124E851-B1CE-4DAD-8C1A-45697701D190}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{A5CC96EF-1E35-4836-9EE6-11F114D3E41D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{6D4A81AD-7209-4848-AFF5-232E4E3FDF41}"
ProjectSection(SolutionItems) = preProject
.github\workflows\ci.yml = .github\workflows\ci.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -81,6 +90,10 @@ Global
{53B48FC5-E76D-4442-9259-DB57CDB53232}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53B48FC5-E76D-4442-9259-DB57CDB53232}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53B48FC5-E76D-4442-9259-DB57CDB53232}.Release|Any CPU.Build.0 = Release|Any CPU
{D124E851-B1CE-4DAD-8C1A-45697701D190}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D124E851-B1CE-4DAD-8C1A-45697701D190}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D124E851-B1CE-4DAD-8C1A-45697701D190}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D124E851-B1CE-4DAD-8C1A-45697701D190}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -94,6 +107,8 @@ Global
{A836BDA9-335E-49E1-AA68-DA3D8B6DABA0} = {B03B3086-D197-4B32-9AE2-8536C345EA2D}
{7E46171B-8E2F-42EB-8C10-526578F9BED0} = {91E482DE-E1E7-4CE1-9511-C0AF07F3648A}
{53B48FC5-E76D-4442-9259-DB57CDB53232} = {E684AE47-A861-44F3-A648-A512652ED9C5}
{D124E851-B1CE-4DAD-8C1A-45697701D190} = {91E482DE-E1E7-4CE1-9511-C0AF07F3648A}
{6D4A81AD-7209-4848-AFF5-232E4E3FDF41} = {A5CC96EF-1E35-4836-9EE6-11F114D3E41D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EB6672D6-318E-493E-8B60-77F5A7A90E66}
Expand Down
1 change: 1 addition & 0 deletions seq-app-mail.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Comparand/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=formattable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=nblumhardt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=seqcli/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=seqid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
79 changes: 79 additions & 0 deletions src/Seq.App.Mail.AmazonSes/AmazonSesMailApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Amazon.SimpleEmail.Model;
using MimeKit;
using Seq.Apps;
using Seq.Mail;

// ReSharper disable UnusedAutoPropertyAccessor.Global

namespace Seq.App.Mail.AmazonSes;

[SeqApp("Amazon Simple Email Service (SES) Mail",
Description = "Send events and notifications by email, using the Amazon Simple Email Service (SES) API.")]
public class AmazonSesMailApp: MailApp
{
readonly IAmazonSesMailGateway _mailGateway;
AmazonSesOptions? _options;

internal AmazonSesMailApp(IAmazonSesMailGateway mailGateway)
{
_mailGateway = mailGateway;
}

public AmazonSesMailApp()
: this (new AmazonSimpleEmailServiceClientMailGateway())
{
}

[SeqAppSetting(
DisplayName = "Access key id",
HelpText = "An AWS access key id.")]
public string? AccessKeyId { get; set; }

[SeqAppSetting(
DisplayName = "Secret key",
HelpText = "An AWS secret key.",
InputType = SettingInputType.Password)]
public string? SecretKey { get; set; }

protected override void OnAttached()
{
base.OnAttached();

_options = new AmazonSesOptions(
NormalizeOption(AccessKeyId) ?? throw new InvalidOperationException("An access key id is required."),
NormalizeOption(SecretKey) ?? throw new InvalidOperationException("A secret key is required."));
}

protected override async Task SendAsync(MimeMessage message, CancellationToken cancel)
{
const string charset = "UTF-8";
var subject = new Content { Data = message.Subject, Charset = charset };

var body = new Body
{
Html = message.HtmlBody != null ? new Content { Data = message.HtmlBody, Charset = charset } : null,
Text = message.TextBody != null ? new Content { Data = message.TextBody, Charset = charset } : null
};

var sesMessage = new Message
{
Subject = subject,
Body = body
};

var destination = new Destination { ToAddresses = message.To.Select(addr => addr.ToString()).ToList() };

var request = new SendEmailRequest
{
Source = message.From.Single().ToString(),
Destination = destination,
Message = sesMessage
};

await _mailGateway.SendAsync(_options!, request, cancel);
}
}
13 changes: 13 additions & 0 deletions src/Seq.App.Mail.AmazonSes/AmazonSesOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Seq.App.Mail.AmazonSes;

class AmazonSesOptions
{
public string AccessKeyId { get; }
public string SecretKey { get; }

public AmazonSesOptions(string accessKeyId, string secretKey)
{
AccessKeyId = accessKeyId;
SecretKey = secretKey;
}
}
Loading
Loading