Skip to content

Commit 3c5a21d

Browse files
committed
automatic merge to finish v1.1.0
2 parents 6b51161 + d31221c commit 3c5a21d

File tree

25 files changed

+1365
-751
lines changed

25 files changed

+1365
-751
lines changed

.github/workflows/build.yml

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,23 @@ on:
88
- 'v*'
99

1010
jobs:
11-
build:
11+
build-linux:
1212
runs-on: ubuntu-latest
1313
permissions:
1414
contents: write
1515
packages: write
16+
outputs:
17+
version: ${{ steps.package-version.outputs.current-version }}
1618

1719
steps:
1820
- uses: actions/checkout@v6
1921

20-
- uses: docker/login-action@v3
22+
- uses: docker/login-action@v4
2123
with:
2224
username: ${{ secrets.DOCKERHUB_USERNAME }}
2325
password: ${{ secrets.DOCKERHUB_TOKEN }}
2426

25-
- uses: docker/login-action@v3
27+
- uses: docker/login-action@v4
2628
with:
2729
registry: ghcr.io
2830
username: ${{ github.actor }}
@@ -33,7 +35,7 @@ jobs:
3335
with:
3436
path: frontend
3537

36-
- uses: docker/build-push-action@v6
38+
- uses: docker/build-push-action@v7
3739
id: build
3840
with:
3941
context: .
@@ -51,20 +53,110 @@ jobs:
5153
docker rm extract
5254
chmod +x ./mailfang-linux-amd64
5355
54-
- name: prepare release notes from changelog
56+
- name: Upload Linux binary
57+
uses: actions/upload-artifact@v6
58+
with:
59+
name: mailfang-linux-amd64
60+
path: ./mailfang-linux-amd64
61+
if-no-files-found: error
62+
63+
build-macos:
64+
runs-on: macos-latest
65+
strategy:
66+
matrix:
67+
include:
68+
- target: x86_64-apple-darwin
69+
asset: macos-x86_64
70+
- target: aarch64-apple-darwin
71+
asset: macos-arm64
72+
73+
steps:
74+
- uses: actions/checkout@v6
75+
76+
- uses: actions/setup-node@v6
77+
with:
78+
node-version: 24
79+
cache: npm
80+
cache-dependency-path: frontend/package-lock.json
81+
82+
- name: Build frontend
83+
working-directory: frontend
5584
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
85+
npm ci
86+
npm run build
5887
59-
- name: create and publish release
60-
env:
61-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88+
- name: Install Rust toolchain (rustup)
89+
run: |
90+
rustup toolchain install stable
91+
rustup target add "${{ matrix.target }}"
92+
93+
- name: Build MailFang binary (macOS)
6294
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
95+
cd backend
96+
cargo build --release --features embed-frontend --target "${{ matrix.target }}"
97+
cp "target/${{ matrix.target }}/release/mailfang" "../mailfang-${{ matrix.asset }}"
98+
chmod +x "../mailfang-${{ matrix.asset }}"
99+
100+
- name: Upload macOS binary
101+
uses: actions/upload-artifact@v6
102+
with:
103+
name: mailfang-${{ matrix.asset }}
104+
path: ./mailfang-${{ matrix.asset }}
105+
if-no-files-found: error
106+
107+
create-release:
108+
runs-on: ubuntu-latest
109+
needs:
110+
- build-linux
111+
- build-macos
112+
permissions:
113+
contents: write
114+
env:
115+
TAG: v${{ needs.build-linux.outputs.version }}
116+
117+
steps:
118+
- uses: actions/checkout@v6
119+
120+
- name: Download Linux binary
121+
uses: actions/download-artifact@v8
122+
with:
123+
name: mailfang-linux-amd64
124+
path: dist/mailfang-linux-amd64
125+
126+
- name: Download macOS Intel binary
127+
uses: actions/download-artifact@v8
128+
with:
129+
name: mailfang-macos-x86_64
130+
path: dist/mailfang-macos-x86_64
131+
132+
- name: Download macOS Apple Silicon binary
133+
uses: actions/download-artifact@v8
134+
with:
135+
name: mailfang-macos-arm64
136+
path: dist/mailfang-macos-arm64
137+
138+
- name: Extract changelog for release body
139+
id: changelog
140+
run: |
141+
BODY="$(awk 'found && /^## [0-9]/ { exit } /^## [0-9]/ { found=1 } found' CHANGELOG.md | sed '/^[[:space:]]*$/d')"
142+
echo "body<<EOF" >> "$GITHUB_OUTPUT"
143+
echo "$BODY" >> "$GITHUB_OUTPUT"
144+
echo "EOF" >> "$GITHUB_OUTPUT"
145+
146+
- name: Create or update GitHub Release
147+
uses: ncipollo/release-action@v1
148+
env:
149+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
150+
with:
151+
tag: ${{ env.TAG }}
152+
name: ${{ env.TAG }}
153+
body: ${{ steps.changelog.outputs.body }}
154+
artifacts: |
155+
dist/mailfang-linux-amd64/mailfang-linux-amd64
156+
dist/mailfang-macos-x86_64/mailfang-macos-x86_64
157+
dist/mailfang-macos-arm64/mailfang-macos-arm64
158+
allowUpdates: true
159+
replacesArtifacts: true
68160

69161
- name: deploy
70162
run: |

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## 1.1.0
4+
5+
- adds favicon
6+
- improve heading font
7+
- fix keyboard navigation
8+
- adds build for mac
9+
- show package version
10+
- update dependencies
11+
- improve sqlite settings
12+
- cleanup dockerfile
13+
- fix some security issues
14+
315
## 1.0.0
416

517
Initial release of MailFang. Send emails to mailfang and text them locally.

Dockerfile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ FROM alpine:latest AS runtime
4040
RUN apk add --no-cache \
4141
ca-certificates \
4242
sqlite \
43-
curl \
4443
&& rm -rf /var/cache/apk/*
4544

4645
RUN addgroup -g 1000 appuser && \
@@ -59,7 +58,5 @@ USER appuser
5958
ENV DATABASE_URL=sqlite:///data/mailfang.db
6059
ENV SMTP_HOST=0.0.0.0:2525
6160
ENV WEB_HOST=0.0.0.0:3000
62-
ENV WEB_PORT=3000
63-
ENV SMTP_PORT=2525
6461

6562
CMD ["mailfang"]

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ The services will be reachable via:
5858
### Binary
5959

6060
1. Download the latest binary from the [releases page](https://github.com/cars10/mailfang/releases)
61-
2. Set executable permissions: `chmod +x ./mailfang`
62-
3. Run MailFang: `./mailfang`
61+
2. Set executable permissions: `chmod +x ./mailfang-linux-amd64`
62+
3. Run MailFang: `./mailfang-linux-amd64`
6363

64-
You can view all available configuration options by running `./mailfang --help`.
64+
You can view all available configuration options by running `./mailfang-linux-amd64 --help`.
6565

6666
## Sending emails to MailFang
6767

backend/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
target/
2-
mailfang.db
2+
mailfang.db*

backend/src/compression.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use flate2::read::GzDecoder;
33
use flate2::write::GzEncoder;
44
use std::io::{Read, Write};
55

6+
const MAX_DECOMPRESSED_SIZE: u64 = 100 * 1024 * 1024; // 100 MiB
7+
68
pub fn compress(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
79
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
810
encoder.write_all(data)?;
@@ -11,7 +13,20 @@ pub fn compress(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
1113

1214
pub fn decompress(compressed_data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
1315
let mut decoder = GzDecoder::new(compressed_data);
14-
let mut decompressed = Vec::new();
15-
decoder.read_to_end(&mut decompressed)?;
16+
let mut decompressed = Vec::with_capacity(8192.min(MAX_DECOMPRESSED_SIZE as usize));
17+
let mut buf = [0u8; 8192];
18+
loop {
19+
let n = decoder.read(&mut buf)?;
20+
if n == 0 {
21+
break;
22+
}
23+
if decompressed.len().saturating_add(n) > MAX_DECOMPRESSED_SIZE as usize {
24+
return Err(std::io::Error::new(
25+
std::io::ErrorKind::InvalidData,
26+
"decompression exceeded size limit",
27+
));
28+
}
29+
decompressed.extend_from_slice(&buf[..n]);
30+
}
1631
Ok(decompressed)
1732
}

backend/src/db/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ impl From<ListParams> for ListQuery {
120120
fn from(params: ListParams) -> Self {
121121
Self {
122122
search: params.search,
123-
page: params.page.unwrap_or(1),
124-
per_page: params.per_page.unwrap_or(20),
123+
page: params.page.unwrap_or(1).max(1),
124+
per_page: params.per_page.unwrap_or(20).clamp(1, 100),
125125
}
126126
}
127127
}

backend/src/db/save_email.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ fn create_email_record(
131131
subject: message.subject.clone(),
132132
date: message.date.map(|d| d.naive_utc()),
133133
envelope_from: message.from.clone(),
134-
size: message.size as i32,
134+
size: (message.size.min(i32::MAX as u64)) as i32,
135135
compressed_data,
136136
body_text: Some(message.body_text.clone()),
137137
body_html: Some(message.body_html.clone()),
@@ -241,7 +241,7 @@ fn save_attachments(
241241
filename: attachment.filename.clone(),
242242
content_type: attachment.content_type.clone(),
243243
compressed_data,
244-
size: attachment.data.len() as i32,
244+
size: (attachment.data.len().min(i32::MAX as usize)) as i32,
245245
content_id: attachment.content_id.clone(),
246246
disposition: attachment.disposition.clone(),
247247
created_at: now,

backend/src/db/search_query.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ pub fn parse_search_query(query: &str) -> ParsedSearchQuery {
5151
let mut matched_ranges = Vec::new();
5252

5353
for cap in field_pattern.captures_iter(query) {
54-
let field_name = cap.get(1).unwrap().as_str();
55-
let value = cap.get(2).unwrap().as_str();
56-
let full_match = cap.get(0).unwrap();
54+
let (field_name, value, full_match) = match (cap.get(1), cap.get(2), cap.get(0)) {
55+
(Some(m1), Some(m2), Some(m0)) => (m1.as_str(), m2.as_str(), m0.range()),
56+
_ => continue,
57+
};
5758

5859
if let Some(field) = SearchField::from_str(field_name) {
5960
let normalized_field = match field {
@@ -64,7 +65,7 @@ pub fn parse_search_query(query: &str) -> ParsedSearchQuery {
6465
field: normalized_field,
6566
value: value.to_string(),
6667
});
67-
matched_ranges.push(full_match.range());
68+
matched_ranges.push(full_match);
6869
}
6970
}
7071

backend/src/html.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use regex::{Captures, Regex};
22

33
pub fn insert_into_head(html: &str, content: &str) -> String {
4-
let head_regex = Regex::new(r"(?i)<head[^>]*>").unwrap();
5-
let html_regex = Regex::new(r"(?i)<html[^>]*>").unwrap();
4+
let head_regex = Regex::new(r"(?i)<head[^>]*>").expect("valid head regex");
5+
let html_regex = Regex::new(r"(?i)<html[^>]*>").expect("valid html regex");
66

77
if head_regex.is_match(html) {
88
head_regex

0 commit comments

Comments
 (0)