Skip to content

Commit 0516804

Browse files
committed
automatic merge to finish v1.0.0
2 parents 68cd53a + a7b248f commit 0516804

File tree

7 files changed

+78
-68
lines changed

7 files changed

+78
-68
lines changed

.github/workflows/build.yml

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
build:
1212
runs-on: ubuntu-latest
1313
permissions:
14-
contents: read
14+
contents: write
1515
packages: write
1616

1717
steps:
@@ -28,12 +28,13 @@ jobs:
2828
username: ${{ github.actor }}
2929
password: ${{ secrets.GITHUB_TOKEN }}
3030

31-
- uses: martinbeentjes/npm-get-version-action@v1.3.1
31+
- uses: martinbeentjes/npm-get-version-action@v1
3232
id: package-version
3333
with:
3434
path: frontend
3535

36-
- uses: docker/build-push-action@v5
36+
- uses: docker/build-push-action@v6
37+
id: build
3738
with:
3839
context: .
3940
push: true
@@ -43,6 +44,28 @@ jobs:
4344
cars10/mailfang:${{ steps.package-version.outputs.current-version }}
4445
ghcr.io/cars10/mailfang:${{ steps.package-version.outputs.current-version }}
4546
47+
- name: extract binary from image
48+
run: |
49+
docker create --name extract cars10/mailfang:${{ steps.package-version.outputs.current-version }}
50+
docker cp extract:/usr/bin/mailfang ./mailfang-linux-amd64
51+
docker rm extract
52+
chmod +x ./mailfang-linux-amd64
53+
54+
- name: prepare release notes from changelog
55+
run: |
56+
# Extract first version block (newest) from CHANGELOG.md
57+
awk 'found && /^## [0-9]/ { exit } /^## [0-9]/ { found=1 } found' CHANGELOG.md > release_notes.txt
58+
59+
- name: create and publish release
60+
env:
61+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62+
run: |
63+
VERSION="${{ steps.package-version.outputs.current-version }}"
64+
gh release create "v${VERSION}" \
65+
./mailfang-linux-amd64 \
66+
--title "v${VERSION}" \
67+
--notes-file release_notes.txt
68+
4669
- name: deploy
4770
run: |
4871
curl -X POST https://autodok.cars10k.de/update \

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## 1.0.0
4+
5+
Initial release of MailFang. Send emails to mailfang and text them locally.

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
MailFang is the email testing tool that you've been waiting for. It provides a local smtp server and a modern webui to view your emails.
44

5+
![MailFang screenshot](screenshot.jpg)
6+
57
**[Live Demo](https://demo.mailfang.com)**
68

79
## About
@@ -45,7 +47,7 @@ Use the [existing image](https://hub.docker.com/r/cars10/mailfang) from docker h
4547
docker run --name mailfang \
4648
-p 3000:3000 \
4749
-p 2525:2525 \
48-
-d cars10/mailfang
50+
cars10/mailfang
4951
```
5052

5153
The services will be reachable via:
@@ -71,7 +73,6 @@ To send emails to Mailfang simply configure your email service to use the respec
7173
config.action_mailer.smtp_settings = {
7274
address: "0.0.0.0",
7375
port: 2525,
74-
domain: "example.com",
7576
authentication: "plain"
7677
}
7778
```
@@ -96,7 +97,7 @@ You can configure MailFang via command-line arguments or environment variables.
9697

9798
### Available Options
9899

99-
All configuration options are optional. The smtp server will accept all connection if no credentials are configured.
100+
All configuration options are optional. The smtp server will accept all connections if no credentials are configured.
100101

101102
| Option | Environment Variable | Description | Binary Default | Docker Default |
102103
|--------|---------------------|-------------|----------------|----------------|
@@ -128,7 +129,7 @@ docker run --name mailfang \
128129

129130
MailFang saves emails in a local sqlite database. To persist the data:
130131

131-
When running via docker use `DATABASE_URL=sqlite:///data/mailfang.db` and mount a volume to `/data`.
132+
When running via docker mount a volume to `/data`.
132133

133134
The binary defaults to `./mailfang.db`, change it by using `--database-url sqlite:///path/to/mailfang.db` or via environment variables `DATABASE_URL=sqlite:///path/to/mailfang.db`
134135

@@ -143,7 +144,7 @@ It supports the following authorization methods:
143144
* `LOGIN`
144145
* `CRAM-MD5`
145146

146-
By default it only accepts a maximum of `4` emails at the same time. This is configurable via `--smtp-max-connections 12` or `SMTP_MAX_CONNECTIONS=12`.
147+
By default it accepts a maximum of `4` open connections at the same time. This is configurable via `--smtp-max-connections 12` or `SMTP_MAX_CONNECTIONS=12`.
147148

148149
## Development
149150

@@ -157,6 +158,14 @@ By default it only accepts a maximum of `4` emails at the same time. This is con
157158

158159
Run `make dev` to start the frontend and backend, access the frontend on `http://localhost:5173`.
159160

161+
## Alternatives & Inspiration
162+
163+
MailFang is inspired partly by tools like:
164+
165+
* mailcatcher
166+
* mailhog
167+
* JSX Email
168+
160169
## License
161170

162171
GNU GPL v3

backend/src/main.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,25 @@ impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
1919
for ConnectionOptions
2020
{
2121
fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> {
22-
conn.batch_execute("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 30000;")
23-
.map_err(diesel::r2d2::Error::QueryError)
22+
let map_err = |e| diesel::r2d2::Error::QueryError(e);
23+
// Sleep if the database is busy, up to 2 seconds.
24+
conn.batch_execute("PRAGMA busy_timeout = 2000;")
25+
.map_err(map_err)?;
26+
// Better write-concurrency.
27+
conn.batch_execute("PRAGMA journal_mode = WAL;")
28+
.map_err(map_err)?;
29+
// Fsync only in critical moments.
30+
conn.batch_execute("PRAGMA synchronous = NORMAL;")
31+
.map_err(map_err)?;
32+
// Write WAL changes back every 1000 pages (~1MB WAL). May affect readers if increased.
33+
conn.batch_execute("PRAGMA wal_autocheckpoint = 1000;")
34+
.map_err(map_err)?;
35+
// Free space by truncating possibly massive WAL files from the last run.
36+
conn.batch_execute("PRAGMA wal_checkpoint(TRUNCATE);")
37+
.map_err(map_err)?;
38+
conn.batch_execute("PRAGMA foreign_keys = ON;")
39+
.map_err(map_err)?;
40+
Ok(())
2441
}
2542
}
2643

backend/src/smtp/server.rs

Lines changed: 12 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use super::parser::{EmailAttachment, parse_email_details};
22
use chrono::Utc;
33
use futures::{SinkExt, StreamExt};
4-
use r2d2::Pool;
54
use rand::Rng;
65
use smtp_proto::Request;
76
use std::fmt;
87
use std::net::SocketAddr;
98
use std::sync::Arc;
109
use tokio::net::{TcpListener, TcpStream};
10+
use tokio::sync::Semaphore;
1111
use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
1212
use tracing::{error, info};
1313
use uuid::Uuid;
@@ -27,29 +27,6 @@ pub type Result<T> = std::result::Result<T, SmtpError>;
2727
/// Callback function type for handling received emails
2828
pub type OnReceiveCallback = Arc<dyn Fn(&Email) + Send + Sync>;
2929

30-
#[derive(Debug)]
31-
struct ConnectionSlot;
32-
33-
#[derive(Debug)]
34-
struct ConnectionManager;
35-
36-
impl r2d2::ManageConnection for ConnectionManager {
37-
type Connection = ConnectionSlot;
38-
type Error = std::io::Error;
39-
40-
fn connect(&self) -> std::result::Result<Self::Connection, Self::Error> {
41-
Ok(ConnectionSlot)
42-
}
43-
44-
fn is_valid(&self, _conn: &mut Self::Connection) -> std::result::Result<(), Self::Error> {
45-
Ok(())
46-
}
47-
48-
fn has_broken(&self, _conn: &mut Self::Connection) -> bool {
49-
false
50-
}
51-
}
52-
5330
pub struct SmtpServer {
5431
addr: SocketAddr,
5532
on_receive: Option<OnReceiveCallback>,
@@ -96,51 +73,30 @@ impl SmtpServer {
9673
let listener = TcpListener::bind(self.addr).await?;
9774
let on_receive = self.on_receive.clone();
9875

99-
let manager = ConnectionManager;
100-
let pool = Arc::new(
101-
Pool::builder()
102-
.max_size(self.max_connections as u32)
103-
.build(manager)
104-
.map_err(|e| {
105-
SmtpError::Io(std::io::Error::new(
106-
std::io::ErrorKind::Other,
107-
format!("Failed to create connection pool: {}", e),
108-
))
109-
})?,
110-
);
76+
// Only accept new TCP connections when we have a slot (enforces max_connections at accept time)
77+
let semaphore = Arc::new(Semaphore::new(self.max_connections));
11178

11279
info!(
11380
component = "smtp",
11481
"SMTP server listening on {} (max connections: {})", self.addr, self.max_connections
11582
);
11683

11784
loop {
85+
// Wait for a free slot before accepting; this ensures we never accept more than max_connections
86+
let permit = semaphore
87+
.clone()
88+
.acquire_owned()
89+
.await
90+
.map_err(|_| SmtpError::Io(std::io::Error::new(std::io::ErrorKind::Other, "semaphore closed")))?;
91+
11892
let (stream, peer) = listener.accept().await?;
11993
info!(component = "smtp", peer = %peer, "Connection accepted");
12094
let on_receive = on_receive.clone();
121-
let pool = pool.clone();
122-
12395
let auth_username = self.auth_username.clone();
12496
let auth_password = self.auth_password.clone();
125-
tokio::spawn(async move {
126-
let connection_result = tokio::task::spawn_blocking({
127-
let pool = pool.clone();
128-
move || pool.get()
129-
})
130-
.await;
131-
132-
let _ = match connection_result {
133-
Ok(Ok(conn)) => conn,
134-
Ok(Err(e)) => {
135-
error!(component = "smtp", peer = %peer, "Failed to get connection from pool: {}", e);
136-
return;
137-
}
138-
Err(e) => {
139-
error!(component = "smtp", peer = %peer, "Failed to spawn blocking task: {}", e);
140-
return;
141-
}
142-
};
14397

98+
tokio::spawn(async move {
99+
let _permit = permit; // released when task ends
144100
if let Err(err) =
145101
handle_connection(stream, on_receive, auth_username, auth_password, peer).await
146102
{

release.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ cd backend
1717
sed -e "s/\"version\":\s\".*\"/\"version\": \"$VERSION\"/" -i Cargo.toml
1818
cd ..
1919

20-
git commit -am "bumps version to $VERSION"
20+
git commit --allow-empty -am "bumps version to $VERSION"
2121

2222
git push
2323

@@ -26,7 +26,7 @@ git merge main -m "automatic merge to finish v$VERSION"
2626

2727
git push
2828

29-
git tag -a "v$VERSION"
29+
git tag -a "v$VERSION" -m "v$VERSION"
3030
git push --tags
3131

3232
git checkout main

screenshot.jpg

136 KB
Loading

0 commit comments

Comments
 (0)