Compare commits

...

118 Commits

Author SHA1 Message Date
Ralph Slooten
ee39f33f84 Merge branch 'release/v1.10.2' 2023-12-01 15:34:45 +13:00
Ralph Slooten
8e9476e3df Release v1.10.2 2023-12-01 15:34:44 +13:00
Ralph Slooten
ceb4c03dc3 UI: Enable tag colors by default 2023-12-01 15:30:14 +13:00
Ralph Slooten
1c565dc564 Libs: Update node modules 2023-12-01 15:23:19 +13:00
Ralph Slooten
f2c517f892 Libs: Update Go modules 2023-12-01 15:18:40 +13:00
Ralph Slooten
97f1530c89 Chore: Add favicon fallback font (sans-serif) for unread count
See #210
2023-12-01 15:14:36 +13:00
Ralph Slooten
945da2c75c Chore: Clearer log messages for bound SMTP & HTTP addresses
See #211
2023-12-01 15:03:01 +13:00
Ralph Slooten
2e9d5008c2 Feature: Allow port binding using hostname
See #213
2023-12-01 14:50:03 +13:00
Ralph Slooten
cfcb4f0c97 Merge tag 'v1.10.1' into develop
Release v1.10.1
2023-11-19 15:14:11 +13:00
Ralph Slooten
b1c9fb6cf6 Merge branch 'release/v1.10.1' 2023-11-19 15:14:08 +13:00
Ralph Slooten
daac2fc921 Release v1.10.1 2023-11-19 15:14:07 +13:00
Ralph Slooten
359573c231 Libs: Update node modules 2023-11-19 15:10:24 +13:00
Ralph Slooten
13c72e4fe5 Libs: Update Go modules 2023-11-19 15:10:23 +13:00
Ralph Slooten
ad91c10744 Swagger: Revert BinaryResponse type to string
Go-swagger does not appear to support `"format": "binary"`, and `"$ref": "#/definitions/File"` doesn't seem to be supported. Resolves #188
2023-11-19 15:10:23 +13:00
Ralph Slooten
d013158ac3 Fix: Prevent JavaScript error if message is missing From header (#209) 2023-11-19 15:09:54 +13:00
Ralph Slooten
ef41de06ae Chore: Use NextReader() instead of ReadMessage() for websocket reading (#207)
This prevents against malicious buffer overflows.
2023-11-19 15:09:53 +13:00
Ralph Slooten
e80c230120 Merge branch 'release/v1.10.0' 2023-11-12 00:00:36 +13:00
Ralph Slooten
79dad9920a Release v1.10.0 2023-11-12 00:00:35 +13:00
Ralph Slooten
7244f4e2ff Libs: Update node modules 2023-11-11 23:54:42 +13:00
Ralph Slooten
ab8466ff7e Libs: Update Go modules 2023-11-11 23:51:34 +13:00
Ralph Slooten
a5bec762d4 Feature: Support search query params to /latest endpoints (#206) 2023-11-11 23:48:45 +13:00
Ralph Slooten
4c5b024eca Feature: Option to allow untrusted HTTPS certificates for screenshots & link checking (#204) 2023-11-11 23:10:43 +13:00
Ralph Slooten
74236258db Fix: Correctly close websockets on client disconnect (#207) 2023-11-11 15:32:57 +13:00
Ralph Slooten
ffe6167d96 Feature: Add URL redirect (/view/latest) to view latest message in web UI (#166) 2023-11-02 16:15:45 +13:00
dependabot[bot]
baa9f3be0b Bump actions/setup-node from 3 to 4 (#203)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 22:10:01 +13:00
Ralph Slooten
2605bc5bfb Merge tag 'v1.9.10' into develop
Release v1.9.10
2023-10-31 16:52:22 +13:00
Ralph Slooten
bc963ccfb2 Merge branch 'release/v1.9.10' 2023-10-31 16:52:17 +13:00
Ralph Slooten
8b03d1703a Release v1.9.10 2023-10-31 16:52:17 +13:00
Ralph Slooten
8e4d03c044 Libs: Update node modules 2023-10-31 16:46:17 +13:00
Ralph Slooten
c6c32f232c Libs: Update Go modules 2023-10-31 15:49:50 +13:00
Ralph Slooten
f19ab57e06 Libs: Update caniemail test data 2023-10-31 15:46:25 +13:00
Ralph Slooten
153eb3df53 Fix: Correctly display "About" modal when update check fails (resolves #199) 2023-10-28 17:33:11 +13:00
Ralph Slooten
f29016a175 Docs: Update documentation links 2023-10-25 10:01:03 +13:00
Ralph Slooten
d82b1e731c UI: Fix column width in search view 2023-10-24 09:00:45 +13:00
Ralph Slooten
615db61df3 Grammar changes 2023-10-24 00:35:00 +13:00
Alexander Ofori
8f5ef400d8 Correction to Usage, Testing Mail and Configuring Mail (#197) 2023-10-23 22:44:30 +13:00
Alexander Ofori
2efa206c4f Correction to brew automatic start sentence (#196) 2023-10-22 20:56:12 +13:00
Ralph Slooten
3586abab1c Reduce time limit for stale issues to 14 days 2023-10-20 23:49:23 +13:00
Ralph Slooten
420fc9f511 Merge tag 'v1.9.9' into develop
Release v1.9.9
2023-10-20 23:33:31 +13:00
Ralph Slooten
6a9bf1d99f Merge branch 'release/v1.9.9' 2023-10-20 23:33:27 +13:00
Ralph Slooten
8908706c1c Release v1.9.9 2023-10-20 23:33:25 +13:00
Ralph Slooten
2812c63b01 Libs: update node modules 2023-10-20 23:29:01 +13:00
Ralph Slooten
0849970442 Libs: Update Go modules 2023-10-20 23:21:42 +13:00
Ralph Slooten
140bdd6c20 Feature: Set optional webhook for received messages (#195) 2023-10-20 23:16:56 +13:00
Ralph Slooten
f256d205ed Feature: Reset message date on release (#194)
When releasing a message the date header is now updated with the current date & time.
2023-10-18 17:40:40 +13:00
Ralph Slooten
30c392bcec Chore: Move html2text module to internal/html2text 2023-10-18 16:28:57 +13:00
Ralph Slooten
80bf374d8a Update note on package managers 2023-10-17 22:21:38 +13:00
apreiml
e5ef02e57b Add Arch Linux package installation note (#192) 2023-10-17 22:09:47 +13:00
Ralph Slooten
ccd27e2b94 Merge tag 'v1.9.8' into develop
Release v1.9.8
2023-10-16 21:18:15 +13:00
Ralph Slooten
c5ea550631 Merge branch 'release/v1.9.8' 2023-10-16 21:18:11 +13:00
Ralph Slooten
b4f5aa3640 Release v1.9.8 2023-10-16 21:18:10 +13:00
Ralph Slooten
82d54d354c Libs: Update node modules 2023-10-16 21:17:35 +13:00
Ralph Slooten
7185649bbd Libs: Update Go modules 2023-10-16 21:15:56 +13:00
Ralph Slooten
506400b764 Merge branch 'feature/swagger' into develop 2023-10-16 21:13:17 +13:00
Cyril Jouve
0e01b9ff73 Chore: Replace satori/go.uuid with github.com/google/uuid (#190)
Fixes #189
2023-10-16 19:22:47 +13:00
Ralph Slooten
4c3e073b0c Change swagger BinaryResponse to os.File 2023-10-16 17:34:10 +13:00
Ralph Slooten
e72dd8d9b6 Replace unprintable characters with space in html2text 2023-10-15 22:02:57 +13:00
Ralph Slooten
e564637203 Tests: Add test to validate swagger.json 2023-10-15 20:55:43 +13:00
Ralph Slooten
cded4d25fc Swagger: Update swagger documentation
See #188
2023-10-15 19:24:06 +13:00
Ralph Slooten
eeac32d09b Merge branch 'feature/html2text' into develop 2023-10-14 22:29:26 +13:00
Ralph Slooten
e9d44c55a1 Tests: Add html2text tests 2023-10-14 22:28:52 +13:00
Ralph Slooten
a9fe0d8e58 Chore: Replace html2text modules with simplified internal function
The module microcosm-cc/bluemonday now requires Go v1.21 and is quite frankly an overkill as Mailpit only needs to convert HTML to a single line (no formatting).
2023-10-14 22:28:14 +13:00
Ralph Slooten
93da18778c Merge tag 'v1.9.7' into develop
Release v1.9.7
2023-10-13 23:22:09 +13:00
Ralph Slooten
9b67792669 Merge branch 'release/v1.9.7' 2023-10-13 23:22:06 +13:00
Ralph Slooten
8739428136 Release v1.9.7 2023-10-13 23:22:05 +13:00
Ralph Slooten
97ec3e839b Libs: Update node modules 2023-10-13 23:20:25 +13:00
Ralph Slooten
56d61ae24b Fix: Enable delete button when new messages arrive
See #185
2023-10-13 23:09:49 +13:00
Ralph Slooten
d43560d45b Libs: Downgrade microcosm-cc/bluemonday, revert to Go 1.20 2023-10-13 23:00:05 +13:00
Andrew Minion
a0e69a202a Add docs for brew services 2023-10-13 22:42:37 +13:00
Ralph Slooten
fc95241521 Libs: Update Go modules & minimum Go version (1.21) 2023-10-13 22:35:06 +13:00
Ralph Slooten
831157a52e Shorten release job name to just "Build" 2023-10-06 17:18:38 +13:00
Ralph Slooten
18c3847deb Merge tag 'v1.9.6' into develop
Release v1.9.6
2023-10-06 17:10:33 +13:00
Ralph Slooten
21134c5bbc Merge branch 'release/v1.9.6' 2023-10-06 17:10:29 +13:00
Ralph Slooten
b34877b3ff Release v1.9.6 2023-10-06 17:10:29 +13:00
Ralph Slooten
47d6e319e3 Libs: Update node modules 2023-10-06 17:06:49 +13:00
Ralph Slooten
a64e964c39 Libs: Update Go modules 2023-10-06 17:05:35 +13:00
Ralph Slooten
e5703d0805 UI: Display message previews on separate line (#175) 2023-10-06 17:04:03 +13:00
Ralph Slooten
c004c1065e Merge tag 'v1.9.5' into develop
Release v1.9.5
2023-10-05 17:39:21 +13:00
Ralph Slooten
af93444374 Merge branch 'release/v1.9.5' 2023-10-05 17:39:19 +13:00
Ralph Slooten
840bc94190 Release v1.9.5 2023-10-05 17:39:19 +13:00
Ralph Slooten
4e2d4d6365 Fix: HTML message preview background color when switching themes in Chrome
Fixes  #182
2023-10-05 17:38:26 +13:00
Ralph Slooten
7446f52205 Fix: Correctly detect tags in search (UI) 2023-10-05 17:23:22 +13:00
Ralph Slooten
d4218df1cf Merge branch 'feature/snippets' into develop 2023-10-05 17:04:25 +13:00
Ralph Slooten
2b18b1bee1 Feature: Add reindex subcommand to reindex all messages 2023-10-05 17:04:05 +13:00
Ralph Slooten
a3f83ea5ce Tests: Add message summary tests 2023-10-05 17:02:35 +13:00
Ralph Slooten
52405915fa Tests: Add snippet tests 2023-10-05 17:02:12 +13:00
Ralph Slooten
636918dd0e Feature: Display email previews (#175) 2023-10-05 17:01:13 +13:00
dependabot[bot]
3fb926f015 Bump docker/setup-qemu-action from 2 to 3 (#177)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
2023-10-01 21:27:39 +13:00
dependabot[bot]
0af6850d34 Bump actions/checkout from 3 to 4 (#178)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
2023-10-01 21:24:24 +13:00
dependabot[bot]
66660b9074 Bump docker/setup-buildx-action from 2 to 3 (#179)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
2023-10-01 21:23:52 +13:00
dependabot[bot]
3b43a803af Bump docker/build-push-action from 4 to 5 (#180)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
2023-10-01 21:22:37 +13:00
dependabot[bot]
ec3dd0c196 Bump docker/login-action from 2 to 3 (#181)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
2023-10-01 21:22:09 +13:00
Ralph Slooten
38240ae96d Merge tag 'v1.9.4' into develop
Release v1.9.4
2023-09-29 16:52:04 +13:00
Ralph Slooten
d0087423db Merge branch 'release/v1.9.4' 2023-09-29 16:52:03 +13:00
Ralph Slooten
1ac8e3a79f Release v1.9.4 2023-09-29 16:52:02 +13:00
Ralph Slooten
67dedd8acc Libs: Update node modules 2023-09-29 16:49:42 +13:00
Ralph Slooten
4f6caca352 Libs: Update Go modules 2023-09-29 16:46:49 +13:00
Ralph Slooten
b6fdcd4ec5 Merge branch 'feature/basic-auth-via-env' into develop 2023-09-29 16:44:52 +13:00
Ralph Slooten
044525fcca Chore: Remove some flags deprecated 08/2022 2023-09-29 16:44:03 +13:00
Ralph Slooten
0ab4210640 Feature: Set auth credentials directly from environment variables
Credentials for the UI and SMTP can now be exported via the `MP_UI_AUTH` and `MP_SMTP_AUTH` environment variables. See #173
2023-09-29 16:40:23 +13:00
Ralph Slooten
e902806ea2 UI: Add option to delete a message after release
See #169
2023-09-28 16:05:44 +13:00
Ralph Slooten
f2b6ba0d69 Merge tag 'v1.9.3' into develop
Release v1.9.3
2023-09-27 17:32:17 +13:00
Ralph Slooten
55bdd45247 Merge branch 'release/v1.9.3' 2023-09-27 17:32:15 +13:00
Ralph Slooten
0b3a5fc5d8 Release v1.9.3 2023-09-27 17:32:15 +13:00
Ralph Slooten
3e90391991 Merge branch 'feature/ui-tests' into develop 2023-09-27 17:29:20 +13:00
Ralph Slooten
ae15cac727 Testing: Add endpoints for integration tests
See #166
2023-09-27 17:29:03 +13:00
Ralph Slooten
1020f76bf8 UI: Do not show excluded search tags as "current" in nav 2023-09-26 19:04:04 +13:00
Ralph Slooten
42a1fe1510 UI: Display "Loading messages" instead of "No results" while loading results 2023-09-26 16:51:30 +13:00
Ralph Slooten
628b7e7881 Code cleanup 2023-09-25 22:14:39 +13:00
Ralph Slooten
fe5de77253 Tests: Add more API tests 2023-09-25 22:14:19 +13:00
Ralph Slooten
36eef88885 Merge branch 'feature/structure' into develop 2023-09-25 19:39:08 +13:00
Ralph Slooten
737cff5a96 Chore: Update internal/storage import paths 2023-09-25 19:29:32 +13:00
Ralph Slooten
009a7deaa1 Chore: Move storage package to internal/storage 2023-09-25 19:29:31 +13:00
Ralph Slooten
b6d5a8c182 Chore: Update internal import paths 2023-09-25 19:29:30 +13:00
Ralph Slooten
10224e7c8b Chore: Move utils/* packages to internal/* 2023-09-25 19:29:02 +13:00
Ralph Slooten
d2086922e5 UI: Only queue broadcast events if clients are connected 2023-09-25 16:53:25 +13:00
Ralph Slooten
3c744edd20 Tests: Add tests for ArgsParser & CleanTag 2023-09-25 16:07:11 +13:00
Ralph Slooten
7ed522e596 Merge tag 'v1.9.2' into develop
Release v1.9.2
2023-09-24 19:16:29 +13:00
84 changed files with 2962 additions and 1254 deletions

View File

@@ -8,16 +8,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -30,7 +30,7 @@ jobs:
version_extractor_regex: 'v(.*)$'
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
# platforms: linux/386,linux/amd64,linux/arm,linux/arm64

View File

@@ -12,11 +12,11 @@ jobs:
steps:
- uses: actions/stale@v8.0.0
with:
days-before-issue-stale: 21
days-before-issue-stale: 14
days-before-issue-close: 7
exempt-issue-labels: "enhancement,bug,javascript,docker"
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 21 days with no activity."
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -5,7 +5,7 @@ on:
name: Build & release
jobs:
releases-matrix:
name: Release Go Binary
name: Build
runs-on: ubuntu-latest
strategy:
matrix:
@@ -21,10 +21,10 @@ jobs:
- goarch: arm
goos: windows
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
# build the assets
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'

View File

@@ -1,7 +1,7 @@
name: Tests
on:
pull_request:
branches: [ develop ]
branches: [ develop, 'feature/**' ]
push:
branches: [ develop, 'feature/**' ]
jobs:
@@ -15,8 +15,9 @@ jobs:
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v3
- uses: actions/cache@v3
- uses: actions/checkout@v4
- name: Run Go tests
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
@@ -24,13 +25,20 @@ jobs:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- run: go test ./storage ./server -v
- run: go test ./storage -bench=.
- run: go test ./internal/storage ./server ./internal/tools ./internal/html2text -v
- run: go test ./internal/storage ./internal/html2text -bench=.
# build the assets
- uses: actions/setup-node@v3
- name: Build web UI
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- run: npm install
- run: npm run package
# validate the swagger file
- name: Validate OpenAPI definition
uses: char0n/swagger-editor-validate@v1
with:
definition-file: server/ui/api/v1/swagger.json

View File

@@ -2,6 +2,176 @@
Notable changes to Mailpit will be documented in this file.
## [v1.10.2]
### Chore
- Add favicon fallback font (sans-serif) for unread count
- Clearer log messages for bound SMTP & HTTP addresses
### Feature
- Allow port binding using hostname
### Libs
- Update node modules
- Update Go modules
### UI
- Enable tag colors by default
## [v1.10.1]
### Chore
- Use NextReader() instead of ReadMessage() for websocket reading ([#207](https://github.com/axllent/mailpit/issues/207))
### Fix
- Prevent JavaScript error if message is missing `From` header ([#209](https://github.com/axllent/mailpit/issues/209))
### Libs
- Update node modules
- Update Go modules
### Swagger
- Revert BinaryResponse type to string
## [v1.10.0]
### Feature
- Support search query params to /latest endpoints ([#206](https://github.com/axllent/mailpit/issues/206))
- Option to allow untrusted HTTPS certificates for screenshots & link checking ([#204](https://github.com/axllent/mailpit/issues/204))
- Add URL redirect (`/view/latest`) to view latest message in web UI ([#166](https://github.com/axllent/mailpit/issues/166))
### Fix
- Correctly close websockets on client disconnect ([#207](https://github.com/axllent/mailpit/issues/207))
### Libs
- Update node modules
- Update Go modules
## [v1.9.10]
### Docs
- Update documentation links
### Fix
- Correctly display "About" modal when update check fails (resolves [#199](https://github.com/axllent/mailpit/issues/199))
### Libs
- Update node modules
- Update Go modules
- Update caniemail test data
### UI
- Fix column width in search view
## [v1.9.9]
### Chore
- Move html2text module to internal/html2text
### Feature
- Set optional webhook for received messages ([#195](https://github.com/axllent/mailpit/issues/195))
- Reset message date on release ([#194](https://github.com/axllent/mailpit/issues/194))
### Libs
- update node modules
- Update Go modules
## [v1.9.8]
### Chore
- Replace satori/go.uuid with github.com/google/uuid ([#190](https://github.com/axllent/mailpit/issues/190))
- Replace html2text modules with simplified internal function
### Libs
- Update node modules
- Update Go modules
### Swagger
- Update swagger documentation
### Tests
- Add test to validate swagger.json
- Add html2text tests
## [v1.9.7]
### Fix
- Enable delete button when new messages arrive
### Libs
- Update node modules
- Downgrade microcosm-cc/bluemonday, revert to Go 1.20
- Update Go modules & minimum Go version (1.21)
## [v1.9.6]
### Libs
- Update node modules
- Update Go modules
### UI
- Display message previews on separate line ([#175](https://github.com/axllent/mailpit/issues/175))
## [v1.9.5]
### Feature
- Add `reindex` subcommand to reindex all messages
- Display email previews ([#175](https://github.com/axllent/mailpit/issues/175))
### Fix
- HTML message preview background color when switching themes in Chrome
- Correctly detect tags in search (UI)
### Tests
- Add message summary tests
- Add snippet tests
## [v1.9.4]
### Chore
- Remove some flags deprecated 08/2022
### Feature
- Set auth credentials directly from environment variables
### Libs
- Update node modules
- Update Go modules
### UI
- Add option to delete a message after release
## [v1.9.3]
### Chore
- Update internal/storage import paths
- Move storage package to internal/storage
- Update internal import paths
- Move utils/* packages to internal/*
### Testing
- Add endpoints for integration tests
### Tests
- Add more API tests
- Add tests for ArgsParser & CleanTag
### UI
- Do not show excluded search tags as "current" in nav
- Display "Loading messages" instead of "No results" while loading results
- Only queue broadcast events if clients are connected
## [v1.9.2]
### Fix

View File

@@ -10,46 +10,49 @@ Mailpit is a small, fast, low memory, zero-dependency, multi-platform email test
It acts as an SMTP server, provides a modern web interface to view & test captured emails, and contains an API for automated integration testing.
Mailpit was originally **inspired** by MailHog which is now [no longer maintained](https://github.com/mailhog/MailHog/issues/442#issuecomment-1493415258) and hasn't seen active development for a few years now.
Mailpit was originally **inspired** by MailHog which is [no longer maintained](https://github.com/mailhog/MailHog/issues/442#issuecomment-1493415258) and hasn't seen active development for a few years now.
![Mailpit](https://raw.githubusercontent.com/axllent/mailpit/develop/docs/screenshot.png)
## Features
- Runs entirely from a single binary, no installation required
- SMTP server (default `0.0.0.0:1025`)
- Web UI to view emails (formatted HTML, highlighted HTML source, text, headers, raw source and MIME attachments including image thumbnails)
- HTML check to test & score mail client compatibility with HTML emails
- Link check to test message links (HTML & text) & linked images
- Screenshots of HTML messages via web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTML-screenshots))
- Runs entirely from a single [static binary](https://mailpit.axllent.org/docs/install/)
- Modern web UI to view emails (formatted HTML, highlighted HTML source, text, headers, raw source, and MIME attachments
including image thumbnails), including optional [HTTPS](https://mailpit.axllent.org/docs/configuration/https/)
- Optional [basic authentication](https://mailpit.axllent.org/docs/configuration/frontend-authentication/) for web UI & API
- [HTML check](https://mailpit.axllent.org/docs/usage/html-check/) to test & score mail client compatibility with HTML emails
- [Link check](https://mailpit.axllent.org/docs/usage/link-check/) to test message links (HTML & text) & linked images
- [Create screenshots](https://mailpit.axllent.org/docs/usage/html-screenshots/) of HTML messages via web UI
- Mobile and tablet HTML preview toggle in desktop mode
- Light & dark web UI theme with auto-detect
- Advanced mail search ([see wiki](https://github.com/axllent/mailpit/wiki/Mail-search))
- Message tagging ([see wiki](https://github.com/axllent/mailpit/wiki/Tagging))
- Real-time web UI updates using web sockets for new mail
- Optional browser notifications for new mail (when accessed via either HTTPS or `localhost` only)
- Advanced [mail search](https://mailpit.axllent.org/docs/usage/search-filters/)
- [Message tagging](https://mailpit.axllent.org/docs/usage/tagging/)
- Real-time web UI updates using web sockets for new mail & optional browser notifications for new mail (when accessed
via either HTTPS or `localhost` only)
- SMTP server with optional [STARTTLS & SMTP authentication](https://mailpit.axllent.org/docs/configuration/smtp-authentication/) (including an
"accept any" mode)
- [SMTP relaying](https://mailpit.axllent.org/docs/configuration/smtp-relay/) (message release) - relay messages via a different SMTP server
including an optional allowlist of accepted recipients
- Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size,
easily handling tens of thousands of emails
- Configurable automatic email pruning (default keeps the most recent 500 emails)
- Email storage either in a temporary or persistent database ([see wiki](https://github.com/axllent/mailpit/wiki/Email-storage))
- Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size, easily handling tens of thousands of emails
- SMTP relaying / message release - relay messages via a different SMTP server including an optional allowlist of accepted recipients ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-relay))
- Optional SMTP with STARTTLS & SMTP authentication, including an "accept anything" mode ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication))
- Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS))
- Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication))
- A simple REST API ([see docs](docs/apiv1/README.md))
- Multi-architecture [Docker images](https://github.com/axllent/mailpit/wiki/Docker-images)
- A simple [REST API](https://mailpit.axllent.org/docs/api-v1/) for integration testing
- Optional [webhook](https://mailpit.axllent.org/docs/integration/webhook/) for received messages
- Multi-architecture [Docker images](https://mailpit.axllent.org/docs/install/docker/)
## Installation
The Mailpit web UI listens by default on `http://0.0.0.0:8025`, and the SMTP port on `0.0.0.0:1025`.
The Mailpit web UI listens by default on `http://0.0.0.0:8025` and the SMTP port on `0.0.0.0:1025`.
Mailpit runs as a single binary and can be installed in different ways:
### Install via Brew (Mac)
### Install via package managers
Install Mailpit with `brew install mailpit`.
- **Mac**: `brew install mailpit` (to run automatically in the background: `brew services start mailpit`)
- **Arch Linux**: available in the AUR as `mailpit`
- **FreeBSD**: `pkg install mailpit`
### Install via bash script (Linux & Mac)
@@ -63,30 +66,33 @@ sudo bash < <(curl -sL https://raw.githubusercontent.com/axllent/mailpit/develop
### Download static binary (Windows, Linux and Mac)
Static binaries can always be found on the [releases](https://github.com/axllent/mailpit/releases/latest). The `mailpit` binary can extracted and copied to your `$PATH`, or simply run as `./mailpit`.
Static binaries can always be found on the [releases](https://github.com/axllent/mailpit/releases/latest). The `mailpit` binary can be extracted and copied to your `$PATH`, or simply run as `./mailpit`.
### Docker
See [Docker instructions](https://github.com/axllent/mailpit/wiki/Docker-images) for 386, amd64 & arm64 images.
See [Docker instructions](https://mailpit.axllent.org/docs/install/docker/) for 386, amd64 & arm64 images.
### Compile from source
To build Mailpit from source see [building from source](https://github.com/axllent/mailpit/wiki/Building-from-source).
To build Mailpit from source, see [Building from source](https://mailpit.axllent.org/docs/install/source/).
## Usage
Run `mailpit -h` to see options. More information can be seen in [the docs](https://github.com/axllent/mailpit/wiki/Runtime-options).
Run `mailpit -h` to see options. More information can be seen in [the docs](https://mailpit.axllent.org/docs/configuration/runtime-options/).
If installed using homebrew, you may run `brew services start mailpit` to always run mailpit automatically.
### Testing Mailpit
Please refer to [the documentation](https://github.com/axllent/mailpit/wiki/Testing-Mailpit) of how to easily test email delivery to Mailpit.
Please refer to [the documentation](https://mailpit.axllent.org/docs/install/testing/) on how to easily test email delivery to Mailpit.
### Configuring sendmail
Mailpit's SMTP server (by default on port 1025), so you will likely need to configure your sending application to deliver mail via that port. A common MTA (Mail Transfer Agent) that delivers system emails to a SMTP server is `sendmail`, used by many applications including PHP. Mailpit can also act as substitute for sendmail. For instructions of how to set this up, please refer to the [sendmail documentation](https://github.com/axllent/mailpit/wiki/Configuring-sendmail).
Mailpit's SMTP server (default on port 1025), so you will likely need to configure your sending application to deliver mail via that port.
A common MTA (Mail Transfer Agent) that delivers system emails to an SMTP server is `sendmail`, used by many applications, including PHP.
Mailpit can also act as substitute for sendmail. For instructions on how to set this up, please refer to the [sendmail documentation](https://mailpit.axllent.org/docs/install/sendmail/).

36
cmd/reindex.go Normal file
View File

@@ -0,0 +1,36 @@
package cmd
import (
"os"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/spf13/cobra"
)
// reindexCmd represents the reindex command
var reindexCmd = &cobra.Command{
Use: "reindex <database>",
Short: "Reindex the database",
Long: `This will reindex all messages in the entire database.
If you have several thousand messages in your mailbox, then it is advised to shut down
Mailpit while you reindex as this process will likely result in database locking issues.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
config.DataFile = args[0]
config.MaxMessages = 0
if err := storage.InitDB(); err != nil {
logger.Log().Error(err)
os.Exit(1)
}
storage.ReindexAll()
},
}
func init() {
rootCmd.AddCommand(reindexCmd)
}

View File

@@ -8,10 +8,12 @@ import (
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/server"
"github.com/axllent/mailpit/server/smtpd"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/server/webhook"
"github.com/spf13/cobra"
)
@@ -27,7 +29,7 @@ It acts as an SMTP server, and provides a web interface to view all captured ema
Documentation:
https://github.com/axllent/mailpit
https://github.com/axllent/mailpit/wiki`,
https://mailpit.axllent.org/docs/`,
Run: func(_ *cobra.Command, _ []string) {
if err := config.VerifyConfig(); err != nil {
logger.Log().Error(err.Error())
@@ -91,7 +93,7 @@ func init() {
rootCmd.Flags().BoolVar(&config.DisableHTMLCheck, "disable-html-check", config.DisableHTMLCheck, "Disable the HTML check functionality (web UI & API)")
rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts")
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI authentication")
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI & API authentication")
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key")
rootCmd.Flags().StringVar(&config.UITLSKey, "ui-tls-key", config.UITLSKey, "TLS key for web UI (HTTPS) - requires ui-tls-cert")
@@ -104,27 +106,15 @@ func init() {
rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages")
rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)")
rootCmd.Flags().StringVar(&config.WebhookURL, "webhook-url", config.WebhookURL, "Send a webhook request for new messages")
rootCmd.Flags().IntVar(&webhook.RateLimit, "webhook-limit", webhook.RateLimit, "Limit webhook requests per second")
rootCmd.Flags().BoolVar(&config.AllowUntrustedTLS, "allow-untrusted-tls", config.AllowUntrustedTLS, "Do not verify HTTPS certificates (link checker & screenshots)")
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters")
rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
// deprecated flags 2022/08/06
rootCmd.Flags().StringVarP(&config.UIAuthFile, "auth-file", "a", config.UIAuthFile, "A password file for web UI authentication")
rootCmd.Flags().StringVar(&config.UITLSCert, "ssl-cert", config.UITLSCert, "SSL certificate - requires ssl-key")
rootCmd.Flags().StringVar(&config.UITLSKey, "ssl-key", config.UITLSKey, "SSL key - requires ssl-cert")
rootCmd.Flags().Lookup("auth-file").Hidden = true
rootCmd.Flags().Lookup("auth-file").Deprecated = "use --ui-auth-file"
rootCmd.Flags().Lookup("ssl-cert").Hidden = true
rootCmd.Flags().Lookup("ssl-cert").Deprecated = "use --ui-tls-cert"
rootCmd.Flags().Lookup("ssl-key").Hidden = true
rootCmd.Flags().Lookup("ssl-key").Deprecated = "use --ui-tls-key"
// deprecated flags 2022/08/30
rootCmd.Flags().StringVar(&config.DataFile, "data", config.DataFile, "Database file to store persistent data")
rootCmd.Flags().Lookup("data").Hidden = true
rootCmd.Flags().Lookup("data").Deprecated = "use --db-file"
// deprecated flags 2023/03/12
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-ssl-cert", config.UITLSCert, "SSL certificate for web UI - requires ui-ssl-key")
rootCmd.Flags().StringVar(&config.UITLSKey, "ui-ssl-key", config.UITLSKey, "SSL key for web UI - requires ui-ssl-cert")
@@ -142,10 +132,8 @@ func init() {
// Load settings from environment
func initConfigFromEnv() {
// defaults from envars if provided
if len(os.Getenv("MP_DATA_FILE")) > 0 {
config.DataFile = os.Getenv("MP_DATA_FILE")
}
// inherit from environment if provided
config.DataFile = os.Getenv("MP_DATA_FILE")
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")
}
@@ -160,26 +148,16 @@ func initConfigFromEnv() {
}
// UI
if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 {
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
}
if len(os.Getenv("MP_UI_TLS_CERT")) > 0 {
config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
}
if len(os.Getenv("MP_UI_TLS_KEY")) > 0 {
config.UITLSKey = os.Getenv("MP_UI_TLS_KEY")
}
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
auth.SetUIAuth(os.Getenv("MP_UI_AUTH"))
config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
config.UITLSKey = os.Getenv("MP_UI_TLS_KEY")
// SMTP
if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 {
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
}
if len(os.Getenv("MP_SMTP_TLS_CERT")) > 0 {
config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
}
if len(os.Getenv("MP_SMTP_TLS_KEY")) > 0 {
config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
}
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
auth.SetSMTPAuth(os.Getenv("MP_SMTP_AUTH"))
config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") {
config.SMTPAuthAcceptAny = true
}
@@ -191,13 +169,19 @@ func initConfigFromEnv() {
}
// Relay server config
if len(os.Getenv("MP_SMTP_RELAY_CONFIG")) > 0 {
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
}
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
if getEnabledFromEnv("MP_SMTP_RELAY_ALL") {
config.SMTPRelayAllIncoming = true
}
// Webhook
if len(os.Getenv("MP_WEBHOOK_URL")) > 0 {
config.WebhookURL = os.Getenv("MP_WEBHOOK_URL")
}
if len(os.Getenv("MP_WEBHOOK_LIMIT")) > 0 {
webhook.RateLimit, _ = strconv.Atoi(os.Getenv("MP_WEBHOOK_LIMIT"))
}
// Misc options
if len(os.Getenv("MP_WEBROOT")) > 0 {
config.Webroot = os.Getenv("MP_WEBROOT")
@@ -217,6 +201,9 @@ func initConfigFromEnv() {
if getEnabledFromEnv("MP_BLOCK_REMOTE_CSS_AND_FONTS") {
config.BlockRemoteCSSAndFonts = true
}
if getEnabledFromEnv("MP_ALLOW_UNTRUSTED_TLS") {
config.AllowUntrustedTLS = true
}
if getEnabledFromEnv("MP_QUIET") {
logger.QuietLogging = true
}
@@ -227,39 +214,22 @@ func initConfigFromEnv() {
// load deprecated settings from environment and warn
func initDeprecatedConfigFromEnv() {
// deprecated 2022/08/06
if len(os.Getenv("MP_AUTH_FILE")) > 0 {
fmt.Println("ENV MP_AUTH_FILE has been deprecated, use MP_UI_AUTH_FILE")
config.UIAuthFile = os.Getenv("MP_AUTH_FILE")
}
// deprecated 2022/08/06
if len(os.Getenv("MP_SSL_CERT")) > 0 {
fmt.Println("ENV MP_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")
config.UITLSCert = os.Getenv("MP_SSL_CERT")
}
// deprecated 2022/08/06
if len(os.Getenv("MP_SSL_KEY")) > 0 {
fmt.Println("ENV MP_SSL_KEY has been deprecated, use MP_UI_TLS_KEY")
config.UITLSKey = os.Getenv("MP_TLS_KEY")
}
// deprecated 2022/08/28
if len(os.Getenv("MP_DATA_DIR")) > 0 {
fmt.Println("ENV MP_DATA_DIR has been deprecated, use MP_DATA_FILE")
config.DataFile = os.Getenv("MP_DATA_DIR")
}
// deprecated 2023/03/12
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
fmt.Println("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")
config.UITLSCert = os.Getenv("MP_UI_SSL_CERT")
}
// deprecated 2023/03/12
if len(os.Getenv("MP_UI_SSL_KEY")) > 0 {
fmt.Println("ENV MP_UI_SSL_KEY has been deprecated, use MP_UI_TLS_KEY")
config.UITLSKey = os.Getenv("MP_UI_SSL_KEY")
}
// deprecated 2023/03/12
if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 {
fmt.Println("ENV MP_SMTP_CERT has been deprecated, use MP_SMTP_TLS_CERT")
config.SMTPTLSCert = os.Getenv("MP_SMTP_SSL_CERT")
}
// deprecated 2023/03/12
if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 {
fmt.Println("ENV MP_SMTP_KEY has been deprecated, use MP_SMTP_TLS_KEY")
config.SMTPTLSKey = os.Getenv("MP_SMTP_SMTP_KEY")

View File

@@ -6,7 +6,7 @@ import (
"runtime"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/updater"
"github.com/axllent/mailpit/internal/updater"
"github.com/spf13/cobra"
)

View File

@@ -4,15 +4,16 @@ package config
import (
"errors"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/utils/tools"
"github.com/tg123/go-htpasswd"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"gopkg.in/yaml.v3"
)
@@ -38,12 +39,9 @@ var (
// UITLSKey file
UITLSKey string
// UIAuthFile for basic authentication
// UIAuthFile for UI & API authentication
UIAuthFile string
// UIAuth used for authentication
UIAuth *htpasswd.File
// Webroot to define the base path for the UI and API
Webroot = "/"
@@ -56,9 +54,6 @@ var (
// SMTPAuthFile for SMTP authentication
SMTPAuthFile string
// SMTPAuthConfig used for authentication auto-generated from SMTPAuthFile
SMTPAuthConfig *htpasswd.File
// SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication
SMTPAuthAllowInsecure bool
@@ -100,9 +95,15 @@ var (
// Use with extreme caution!
SMTPRelayAllIncoming = false
// WebhookURL for calling
WebhookURL string
// ContentSecurityPolicy for HTTP server - set via VerifyConfig()
ContentSecurityPolicy string
// AllowUntrustedTLS allows untrusted HTTPS connections link checking & screenshot generation
AllowUntrustedTLS bool
// Version is the default application version, updated on release
Version = "dev"
@@ -149,7 +150,7 @@ func VerifyConfig() error {
DataFile = filepath.Join(DataFile, "mailpit.db")
}
re := regexp.MustCompile(`^((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(\[([\da-fA-F:])+\])):\d+$`)
re := regexp.MustCompile(`.*:\d+$`)
if !re.MatchString(SMTPListen) {
return errors.New("SMTP bind should be in the format of <ip>:<port>")
}
@@ -161,12 +162,13 @@ func VerifyConfig() error {
if !isFile(UIAuthFile) {
return fmt.Errorf("HTTP password file not found: %s", UIAuthFile)
}
a, err := htpasswd.New(UIAuthFile, htpasswd.DefaultSystems, nil)
b, err := os.ReadFile(UIAuthFile)
if err != nil {
return err
}
UIAuth = a
if err := auth.SetUIAuth(string(b)); err != nil {
return err
}
}
if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" {
@@ -202,18 +204,21 @@ func VerifyConfig() error {
return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile)
}
if SMTPAuthAcceptAny {
return errors.New("SMTP authentication can either use --smtp-auth-file or --smtp-auth-accept-any")
}
a, err := htpasswd.New(SMTPAuthFile, htpasswd.DefaultSystems, nil)
b, err := os.ReadFile(SMTPAuthFile)
if err != nil {
return err
}
SMTPAuthConfig = a
if err := auth.SetSMTPAuth(string(b)); err != nil {
return err
}
}
if SMTPTLSCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
if auth.SMTPCredentials != nil && SMTPAuthAcceptAny {
return errors.New("SMTP authentication cannot use both credentials and --smtp-auth-accept-any")
}
if SMTPTLSCert == "" && (auth.SMTPCredentials != nil || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
return errors.New("SMTP authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
}
@@ -225,6 +230,10 @@ func VerifyConfig() error {
s := strings.TrimRight(path.Join("/", Webroot, "/"), "/") + "/"
Webroot = s
if WebhookURL != "" && !isValidURL(WebhookURL) {
return fmt.Errorf("Webhook URL does not appear to be a valid URL (%s)", WebhookURL)
}
SMTPTags = []AutoTag{}
if SMTPCLITags != "" {
@@ -351,3 +360,12 @@ func isDir(path string) bool {
return true
}
func isValidURL(s string) bool {
u, err := url.ParseRequestURI(s)
if err != nil {
return false
}
return strings.HasPrefix(u.Scheme, "http")
}

View File

@@ -4,9 +4,9 @@ Mailpit provides a simple REST API to access and delete stored messages.
If the Mailpit server is set to use Basic Authentication, then API requests must use Basic Authentication too.
You can view the Swagger API documentation directly within Mailpit by going to `http://0.0.0.0:8025/api/v1/`.
You can view the Swagger API documentation directly within Mailpit by going to https://mailpit.axllent.org/docs/api-v1/.
The API is split into three main parts:
The API is split into four main parts:
- [Messages](Messages.md) - Listing, deleting & marking messages as read/unread.
- [Message](Message.md) - Return message data & attachments

39
go.mod
View File

@@ -7,25 +7,25 @@ require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/axllent/semver v0.0.1
github.com/disintegration/imaging v1.6.2
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd
github.com/google/uuid v1.4.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/jhillyerd/enmime v1.0.1
github.com/k3a/html2text v1.2.1
github.com/klauspost/compress v1.17.0
github.com/klauspost/compress v1.17.3
github.com/leporo/sqlf v1.4.0
github.com/mhale/smtpd v0.8.0
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/tg123/go-htpasswd v1.2.1
github.com/vanng822/go-premailer v1.20.2
golang.org/x/net v0.15.0
golang.org/x/text v0.13.0
golang.org/x/net v0.19.0
golang.org/x/text v0.14.0
golang.org/x/time v0.5.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.25.0
modernc.org/sqlite v1.27.0
)
require (
@@ -36,13 +36,12 @@ require (
github.com/cznic/ql v1.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -52,18 +51,18 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vanng822/css v1.0.1 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/image v0.12.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/tools v0.13.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/tools v0.16.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.15 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/libc v1.34.11 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.1 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect

91
go.sum
View File

@@ -15,7 +15,7 @@ github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E=
github.com/axllent/semver v0.0.1/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
@@ -51,34 +51,29 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4=
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd h1:PppHBegd3uPZ3Y/Iax/2mlCFJm1w4Qf/zP1MdW4ju2o=
github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v1.0.1 h1:y6RyqIgBOI2hIinOXIzmeB+ITRVls0zTJIm5GwgXnjE=
github.com/jhillyerd/enmime v1.0.1/go.mod h1:LMMbm6oTlzWHghPavqHtOrP/NosVv3l42CUrZjn03/Q=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY=
github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -88,8 +83,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c=
github.com/leporo/sqlf v1.4.0/go.mod h1:pgN9yKsAnQ+2ewhbZogr98RcasUjPsHF3oXwPPhHvBw=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -115,16 +110,10 @@ github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
@@ -152,17 +141,16 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -173,12 +161,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -191,8 +179,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -203,15 +191,16 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -229,16 +218,16 @@ modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/libc v1.34.11 h1:hQDcIUlSG4QAOkXCIQKkaAOV5ptXvkOx4ddbXzgW2JU=
modernc.org/libc v1.34.11/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.1 h1:9J+2/GKTlV503mk3yv8QJ6oEpRCUrRy0ad8TXEPoV8M=
modernc.org/memory v1.7.1/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8=
modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=

69
internal/auth/auth.go Normal file
View File

@@ -0,0 +1,69 @@
// Package auth handles the web UI and SMTP authentication
package auth
import (
"regexp"
"strings"
"github.com/tg123/go-htpasswd"
)
var (
// UICredentials passwords
UICredentials *htpasswd.File
// SMTPCredentials passwords
SMTPCredentials *htpasswd.File
)
// SetUIAuth will set Basic Auth credentials required for the UI & API
func SetUIAuth(s string) error {
var err error
credentials := credentialsFromString(s)
if len(credentials) == 0 {
return nil
}
r := strings.NewReader(strings.Join(credentials, "\n"))
UICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
if err != nil {
return err
}
return nil
}
// SetSMTPAuth will set SMTP credentials
func SetSMTPAuth(s string) error {
var err error
credentials := credentialsFromString(s)
if len(credentials) == 0 {
return nil
}
r := strings.NewReader(strings.Join(credentials, "\n"))
SMTPCredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
if err != nil {
return err
}
return nil
}
func credentialsFromString(s string) []string {
// split string by any whitespace character
re := regexp.MustCompile(`\s+`)
words := re.Split(s, -1)
credentials := []string{}
for _, w := range words {
if w != "" {
credentials = append(credentials, w)
}
}
return credentials
}

View File

@@ -0,0 +1,82 @@
// Package html2text is a simple library to convert HTML to plain text
package html2text
import (
"bytes"
"log"
"regexp"
"strings"
"unicode"
"golang.org/x/net/html"
)
var (
re = regexp.MustCompile(`\s+`)
spaceRe = regexp.MustCompile(`(?mi)<\/(div|p|td|th|h[1-6]|ul|ol|li|address|article|aside|blockquote|dl|dt|footer|header|hr|main|nav|pre|table|thead|tfoot|video)><`)
brRe = regexp.MustCompile(`(?mi)<(br /|br)>`)
imgRe = regexp.MustCompile(`(?mi)<(img)`)
skip = make(map[string]bool)
)
func init() {
skip["script"] = true
skip["title"] = true
skip["head"] = true
skip["link"] = true
skip["meta"] = true
skip["style"] = true
skip["noscript"] = true
}
// Strip will convert a HTML string to plain text
func Strip(h string, includeLinks bool) string {
h = spaceRe.ReplaceAllString(h, "</$1> <")
h = brRe.ReplaceAllString(h, " ")
h = imgRe.ReplaceAllString(h, " <$1")
var buffer bytes.Buffer
doc, err := html.Parse(strings.NewReader(h))
if err != nil {
log.Fatal(err)
}
extract(doc, &buffer, includeLinks)
return clean(buffer.String())
}
func extract(node *html.Node, buff *bytes.Buffer, includeLinks bool) {
if node.Type == html.TextNode {
data := node.Data
if data != "" {
buff.WriteString(data)
}
}
for c := node.FirstChild; c != nil; c = c.NextSibling {
if _, skip := skip[c.Data]; !skip {
if includeLinks && c.Data == "a" {
for _, a := range c.Attr {
if a.Key == "href" && strings.HasPrefix(strings.ToLower(a.Val), "http") {
buff.WriteString(" " + a.Val + " ")
}
}
}
extract(c, buff, includeLinks)
}
}
}
func clean(text string) string {
// replace \uFEFF with space, see https://github.com/golang/go/issues/42274#issuecomment-1017258184
text = strings.ReplaceAll(text, string('\uFEFF'), " ")
// remove non-printable characters
text = strings.Map(func(r rune) rune {
if unicode.IsPrint(r) {
return r
}
return []rune(" ")[0]
}, text)
text = re.ReplaceAllString(text, " ")
return strings.TrimSpace(text)
}

View File

@@ -0,0 +1,250 @@
package html2text
import "testing"
func TestPlain(t *testing.T) {
tests := map[string]string{}
tests["this is a test"] = "this is a test"
tests["thiS IS a Test"] = "thiS IS a Test"
tests["thiS IS a Test :-)"] = "thiS IS a Test :-)"
tests["<h1>This is a test.</h1> "] = "This is a test."
tests["<p>Paragraph 1</p><p>Paragraph 2</p>"] = "Paragraph 1 Paragraph 2"
tests["<h1>Heading</h1><p>Paragraph</p>"] = "Heading Paragraph"
tests["<span>Alpha</span>bet <strong>chars</strong>"] = "Alphabet chars"
tests["<span><b>A</b>lpha</span>bet chars."] = "Alphabet chars."
tests["<table><tr><td>First</td><td>Second</td></table>"] = "First Second"
tests[`<h1>Heading</h1>
<p>Paragraph</p>`] = "Heading Paragraph"
tests[`<h1>Heading</h1><p> <a href="https://github.com">linked text</a></p>`] = "Heading linked text"
// broken html
tests[`<h1>Heading</h3><p> <a href="https://github.com">linked text.`] = "Heading linked text."
for str, expected := range tests {
res := Strip(str, false)
if res != expected {
t.Log("error:", res, "!=", expected)
t.Fail()
}
}
}
func TestWithLinks(t *testing.T) {
tests := map[string]string{}
tests["this is a test"] = "this is a test"
tests["thiS IS a Test"] = "thiS IS a Test"
tests["thiS IS a Test :-)"] = "thiS IS a Test :-)"
tests["<h1>This is a test.</h1> "] = "This is a test."
tests["<p>Paragraph 1</p><p>Paragraph 2</p>"] = "Paragraph 1 Paragraph 2"
tests["<h1>Heading</h1><p>Paragraph</p>"] = "Heading Paragraph"
tests["<span>Alpha</span>bet <strong>chars</strong>"] = "Alphabet chars"
tests["<span><b>A</b>lpha</span>bet chars."] = "Alphabet chars."
tests["<table><tr><td>First</td><td>Second</td></table>"] = "First Second"
tests["<h1>Heading</h1><p>Paragraph</p>"] = "Heading Paragraph"
tests[`<h1>Heading</h1>
<p>Paragraph</p>`] = "Heading Paragraph"
tests[`<h1>Heading</h1><p> <a href="https://github.com">linked text</a></p>`] = "Heading https://github.com linked text"
// broken html
tests[`<h1>Heading</h3><p> <a href="https://github.com">linked text.`] = "Heading https://github.com linked text."
for str, expected := range tests {
res := Strip(str, true)
if res != expected {
t.Log("error:", res, "!=", expected)
t.Fail()
}
}
}
func BenchmarkPlain(b *testing.B) {
for i := 0; i < b.N; i++ {
Strip(htmlTestData, false)
}
}
func BenchmarkLinks(b *testing.B) {
for i := 0; i < b.N; i++ {
Strip(htmlTestData, true)
}
}
var htmlTestData = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" style="font-family: sans-serif; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; box-sizing: border-box;" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<title>[axllent/mailpit] Run failed: .github/workflows/tests.yml - feature/swagger (284335a)</title>
</head>
<body style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;; font-size: 14px; line-height: 1.5; color: #24292e; background-color: #fff; margin: 0;" bgcolor="#fff">
<table align="center" class="container-sm width-full" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; max-width: 544px; margin-right: auto; margin-left: auto; width: 100% !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td class="center p-3" align="center" valign="top" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 16px;">
<center style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<table border="0" cellspacing="0" cellpadding="0" align="center" class="width-full container-md" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; max-width: 768px; margin-right: auto; margin-left: auto; width: 100% !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td align="center" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td height="16" style="font-size: 16px; line-height: 16px; box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">&#160;</td>
</tr>
</tbody>
</table>
<table border="0" cellspacing="0" cellpadding="0" align="left" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td class="text-left" style="box-sizing: border-box; text-align: left !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;" align="left">
<img src="https://github.githubassets.com/images/email/global/octocat-logo.png" alt="GitHub" width="32" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; border-style: none;" />
<h2 class="lh-condensed mt-2 text-normal" style="box-sizing: border-box; margin-top: 8px !important; margin-bottom: 0; font-size: 24px; font-weight: 400 !important; line-height: 1.25 !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
[axllent/mailpit] .github/workflows/tests.yml workflow run
</h2>
</td>
</tr>
</table>
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td height="16" style="font-size: 16px; line-height: 16px; box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">&#160;</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<table width="100%" class="width-full" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; width: 100% !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td class="border rounded-2 d-block" style="box-sizing: border-box; border-radius: 6px !important; display: block !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0; border: 1px solid #e1e4e8;">
<table align="center" class="width-full text-center" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; width: 100% !important; text-align: center !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<table border="0" cellspacing="0" cellpadding="0" align="center" class="width-full" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; width: 100% !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td align="center" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<table align="center" class="border-bottom width-full text-center" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; border-bottom-width: 1px !important; border-bottom-color: #e1e4e8 !important; border-bottom-style: solid !important; width: 100% !important; text-align: center !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td class="d-block px-3 pt-3 p-sm-4" style="box-sizing: border-box; display: block !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 24px;">
<table border="0" cellspacing="0" cellpadding="0" align="center" class="width-full" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; width: 100% !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td align="center" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<img src="https://github.githubassets.com/images/email/icons/actions.png" width="56" height="56" alt="" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; border-style: none;" />
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td height="12" style="font-size: 12px; line-height: 12px; box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">&#160;</td>
</tr>
</tbody>
</table>
<h3 class="lh-condensed" style="box-sizing: border-box; margin-top: 0; margin-bottom: 0; font-size: 20px; font-weight: 600; line-height: 1.25 !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">.github/workflows/tests.yml: No jobs were run</h3>
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td height="16" style="font-size: 16px; line-height: 16px; box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">&#160;</td>
</tr>
</tbody>
</table>
<table border="0" cellspacing="0" cellpadding="0" align="center" class="width-full" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; width: 100% !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td align="center" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<table width="100%" border="0" cellspacing="0" cellpadding="0" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<table border="0" cellspacing="0" cellpadding="0" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td align="center" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<!--[if mso]> <table><tr><td align="center" bgcolor="#28a745"> <![endif]-->
<a href="https://github.com/axllent/mailpit/actions/runs/6522820865" target="_blank" rel="noopener noreferrer" class="btn btn-large btn-primary" style="background-color: #1f883d !important; box-sizing: border-box; color: #fff; text-decoration: none; position: relative; display: inline-block; font-size: inherit; font-weight: 500; line-height: 1.5; white-space: nowrap; vertical-align: middle; cursor: pointer; -webkit-user-select: none; user-select: none; border-radius: .5em; -webkit-appearance: none; appearance: none; box-shadow: 0 1px 0 rgba(27,31,35,.1),inset 0 1px 0 rgba(255,255,255,.03); transition: background-color .2s cubic-bezier(0.3, 0, 0.5, 1); font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: .75em 1.5em; border: 1px solid #1f883d;">View workflow run</a>
<!--[if mso]> </td></tr></table> <![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td height="32" style="font-size: 32px; line-height: 32px; box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">&#160;</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table border="0" cellspacing="0" cellpadding="0" align="center" class="width-full text-center" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; width: 100% !important; text-align: center !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td align="center" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td height="16" style="font-size: 16px; line-height: 16px; box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">&#160;</td>
</tr>
</tbody>
</table>
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td height="16" style="font-size: 16px; line-height: 16px; box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">&#160;</td>
</tr>
</tbody>
</table>
<p class="f5 text-gray-light" style="box-sizing: border-box; margin-top: 0; margin-bottom: 10px; color: #6a737d !important; font-size: 14px !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;"> </p><p style="font-size: small; -webkit-text-size-adjust: none; color: #666; box-sizing: border-box; margin-top: 0; margin-bottom: 10px; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">&#8212;<br style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;" />You are receiving this because you are subscribed to this thread.<br style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;" /><a href="https://github.com/settings/notifications" style="background-color: transparent; box-sizing: border-box; color: #0366d6; text-decoration: none; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">Manage your GitHub Actions notifications</a></p>
</td>
</tr>
</table>
<table border="0" cellspacing="0" cellpadding="0" align="center" class="width-full text-center" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; width: 100% !important; text-align: center !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td align="center" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">
<td height="16" style="font-size: 16px; line-height: 16px; box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important; padding: 0;">&#160;</td>
</tr>
</tbody>
</table>
<p class="f6 text-gray-light" style="box-sizing: border-box; margin-top: 0; margin-bottom: 10px; color: #6a737d !important; font-size: 12px !important; font-family: -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot; !important;">GitHub, Inc. &#12539;88 Colin P Kelly Jr Street &#12539;San Francisco, CA 94107</p>
</td>
</tr>
</table>
</center>
</td>
</tr>
</table>
<!-- prevent Gmail on iOS font size manipulation -->
<div style="display: none; white-space: nowrap; box-sizing: border-box; font: 15px/0 apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;"> &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; </div>
</body>
</html>`

View File

@@ -1,6 +1,6 @@
{
"api_version":"1.0.4",
"last_update_date":"2023-09-22 13:57:52 +0000",
"last_update_date":"2023-10-10 15:06:18 +0000",
"nicenames":{"family":{"gmail":"Gmail","outlook":"Outlook","yahoo":"Yahoo! Mail","apple-mail":"Apple Mail","aol":"AOL","thunderbird":"Mozilla Thunderbird","microsoft":"Microsoft","samsung-email":"Samsung Email","sfr":"SFR","orange":"Orange","protonmail":"ProtonMail","hey":"HEY","mail-ru":"Mail.ru","fastmail":"Fastmail","laposte":"LaPoste.net","t-online-de":"T-online.de","free-fr":"Free.fr","gmx":"GMX","web-de":"WEB.DE","ionos-1and1":"1&1","rainloop":"RainLoop"},"platform":{"desktop-app":"Desktop","desktop-webmail":"Desktop Webmail","mobile-webmail":"Mobile Webmail","webmail":"Webmail","ios":"iOS","android":"Android","windows":"Windows","macos":"macOS","windows-mail":"Windows Mail","outlook-com":"Outlook.com"},"support":{"supported":"Supported","mitigated":"Partially supported","unsupported":"Not supported","unknown":"Support unknown","mixed":"Mixed support"},"category":{"html":"HTML","css":"CSS","image":"Image formats","others":"Others"}},
"data":[
{
@@ -169,7 +169,7 @@
"description":"This media query tests whether the user's input device[s] (i.e mouse, trackpad etc.) can hover over elements",
"url":"https://www.caniemail.com/features/css-at-media-hover/",
"category":"css",
"tags":[],
"tags":["accessibility"],
"keywords":"media, media query, hover, any-hover",
"last_test_date":"2022-08-29",
"test_url":"https://www.caniemail.com/tests/css-media-hover.html",
@@ -201,7 +201,7 @@
"description":"This media query allows to theme for system light and dark mode.",
"url":"https://www.caniemail.com/features/css-at-media-prefers-color-scheme/",
"category":"css",
"tags":[],
"tags":["accessibility"],
"keywords":"media queries, media query, media feature, dark mode, light mode",
"last_test_date":"2023-03-08",
"test_url":"https://www.caniemail.com/tests/css-media-prefers-color-scheme.html",
@@ -1561,7 +1561,7 @@
"description":null,
"url":"https://www.caniemail.com/features/css-pseudo-class-active/",
"category":"css",
"tags":[],
"tags":["accessibility"],
"keywords":"pseudo-class",
"last_test_date":"2019-10-28",
"test_url":"https://www.caniemail.com/tests/css-selectors-pseudo-classes.html",
@@ -1625,7 +1625,7 @@
"description":null,
"url":"https://www.caniemail.com/features/css-pseudo-class-focus/",
"category":"css",
"tags":[],
"tags":["accessibility"],
"keywords":"pseudo-class",
"last_test_date":"2019-10-28",
"test_url":"https://www.caniemail.com/tests/css-selectors-pseudo-classes.html",
@@ -1657,7 +1657,7 @@
"description":null,
"url":"https://www.caniemail.com/features/css-pseudo-class-hover/",
"category":"css",
"tags":[],
"tags":["accessibility"],
"keywords":"rollover, pseudo-class",
"last_test_date":"2019-10-23",
"test_url":"https://www.caniemail.com/tests/css-selectors-pseudo-classes.html",
@@ -2683,10 +2683,10 @@
"category":"css",
"tags":[],
"keywords":"unit, vh",
"last_test_date":"2020-02-25",
"last_test_date":"2023-10-01",
"test_url":"https://www.caniemail.com/tests/css-units.html",
"test_results_url":"",
"stats":{"apple-mail":{"macos":{"13":"y"},"ios":{"13":"y"}},"gmail":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"},"mobile-webmail":{"2020-02":"y"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"n"},"macos":{"2016":"y"},"outlook-com":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"yahoo":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"n"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"a #2"}},"web-de":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"a #2"}},"ionos-1and1":{"desktop-webmail":{"2022-08":"y #1"},"android":{"2022-08":"y"}}},
"test_results_url":"https://app.emailonacid.com/app/acidtest/AhzTJnsoWULAInwe2B8h7uzlsa6vGOgAkVK1VA6BbuKaW/list",
"stats":{"apple-mail":{"macos":{"13":"y"},"ios":{"13":"y","15":"a #2","17":"a #2"}},"gmail":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"},"mobile-webmail":{"2020-02":"y"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"n"},"macos":{"2016":"y"},"outlook-com":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"yahoo":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"n"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"a #2"}},"web-de":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"a #2"}},"ionos-1and1":{"desktop-webmail":{"2022-08":"y #1"},"android":{"2022-08":"y"}}},
"notes":"",
"notes_by_num":{"1":"Buggy. Can affect the preview window size, meaning content can get lost.","2":"Buggy. Value resolves to zero"}
},
@@ -2867,6 +2867,22 @@
"notes_by_num":null
},
{
"slug":"html-abbr",
"title":"<abbr> element",
"description":"Represents an abbreviation or acronym.",
"url":"https://www.caniemail.com/features/html-abbr/",
"category":"html",
"tags":[],
"keywords":null,
"last_test_date":"2023-09-13",
"test_url":"https://www.caniemail.com/tests/html-abbr.html",
"test_results_url":"https://testi.at/proj/xlp4una8f926u48oco8",
"stats":{"apple-mail":{"macos":{"2023-09":"y"},"ios":{"2023-09":"y"}},"gmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"},"mobile-webmail":{"2023-09":"y"}},"orange":{"desktop-webmail":{"2023-10":"n"},"ios":{"2023-10":"n"},"android":{"2023-09":"u"}},"outlook":{"windows":{"2013":"n #1","2016":"n #1","2019":"n #1","2021":"n #1"},"windows-mail":{"2023-09":"n #1"},"macos":{"2023-09":"y"},"outlook-com":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"samsung-email":{"android":{"2023-09":"y"}},"sfr":{"desktop-webmail":{"2023-10":"y"},"ios":{"2023-10":"y"},"android":{"2023-09":"u"}},"thunderbird":{"macos":{"2023-09":"y"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"protonmail":{"desktop-webmail":{"2023-10":"y"},"ios":{"2023-10":"y"},"android":{"2023-09":"u"}},"hey":{"desktop-webmail":{"2023-10":"n"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-10":"y"}},"laposte":{"desktop-webmail":{"2023-10":"y"}},"free-fr":{"desktop-webmail":{"2023-10":"y"}},"t-online-de":{"desktop-webmail":{"2023-09":"y"}},"gmx":{"desktop-webmail":{"2023-09":"y"}},"web-de":{"desktop-webmail":{"2023-09":"y"}}},
"notes":null,
"notes_by_num":{"1":"Buggy. Element's content are still kept."}
},
{
"slug":"html-address",
"title":"address",
@@ -3155,6 +3171,22 @@
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
},
{
"slug":"html-dfn",
"title":"<def> element",
"description":"It is used to identify a term that is going to be described within the content.",
"url":"https://www.caniemail.com/features/html-dfn/",
"category":"html",
"tags":[],
"keywords":null,
"last_test_date":"2023-09-11",
"test_url":"https://www.caniemail.com/tests/html-dfn.html",
"test_results_url":"https://testi.at/proj/gy2dfo4j19d4176d08y",
"stats":{"apple-mail":{"macos":{"2023-09":"y"},"ios":{"2023-09":"y"}},"gmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"},"mobile-webmail":{"2023-09":"y"}},"orange":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"outlook":{"windows":{"2013":"a #1","2016":"a #1","2019":"a #1","2021":"a #1"},"windows-mail":{"2023-09":"a #1"},"macos":{"16.56":"y"},"outlook-com":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"samsung-email":{"android":{"2023-09":"y"}},"sfr":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"thunderbird":{"macos":{"2023-09":"y"}},"aol":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"yahoo":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"protonmail":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"hey":{"desktop-webmail":{"2023-09":"u"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"u"}},"laposte":{"desktop-webmail":{"2023-09":"u"}},"free-fr":{"desktop-webmail":{"2023-09":"u"}},"t-online-de":{"desktop-webmail":{"2023-09":"n"}},"gmx":{"desktop-webmail":{"2023-09":"y"}},"web-de":{"desktop-webmail":{"2023-09":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. The `title` attribute is removed."}
},
{
"slug":"html-dialog",
"title":"<dialog> element",

View File

@@ -2,7 +2,7 @@ package htmlcheck
import (
"fmt"
"io/ioutil"
"io"
"net/http"
"net/url"
"strings"
@@ -10,8 +10,8 @@ import (
"github.com/PuerkitoBio/goquery"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/utils/tools"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/vanng822/go-premailer/premailer"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
@@ -204,7 +204,7 @@ func downloadToBytes(url string) ([]byte, error) {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@@ -5,7 +5,7 @@ import (
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/axllent/mailpit/utils/tools"
"github.com/axllent/mailpit/internal/tools"
)
// HTML tests

View File

@@ -6,8 +6,8 @@ import (
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/tools"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
)
var linkRe = regexp.MustCompile(`(?m)\b(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:'!\/~+#-]*[\w@?^=%&\/~+#-])`)

View File

@@ -1,13 +1,14 @@
package linkcheck
import (
"crypto/tls"
"net/http"
"regexp"
"sync"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/logger"
)
func getHTTPStatuses(links []string, followRedirects bool) []Link {
@@ -59,8 +60,15 @@ func doHead(link string, followRedirects bool) (int, error) {
timeout := time.Duration(10 * time.Second)
tr := &http.Transport{}
if config.AllowUntrustedTLS {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
client := http.Client{
Timeout: timeout,
Timeout: timeout,
Transport: tr,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if followRedirects {
return nil

View File

@@ -18,14 +18,15 @@ import (
"syscall"
"time"
"github.com/GuiaBolso/darwin"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server/webhook"
"github.com/axllent/mailpit/server/websockets"
"github.com/axllent/mailpit/utils/logger"
"github.com/google/uuid"
"github.com/jhillyerd/enmime"
"github.com/klauspost/compress/zstd"
"github.com/leporo/sqlf"
uuid "github.com/satori/go.uuid"
// sqlite (native) - https://gitlab.com/cznic/sqlite
_ "modernc.org/sqlite"
@@ -42,81 +43,8 @@ var (
// zstd compression encoder & decoder
dbEncoder, _ = zstd.NewWriter(nil)
dbDecoder, _ = zstd.NewReader(nil)
dbMigrations = []darwin.Migration{
{
Version: 1.0,
Description: "Creating tables",
Script: `CREATE TABLE IF NOT EXISTS mailbox (
Sort INTEGER PRIMARY KEY AUTOINCREMENT,
ID TEXT NOT NULL,
Data BLOB,
Search TEXT,
Read INTEGER
);
CREATE INDEX IF NOT EXISTS idx_sort ON mailbox (Sort);
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
CREATE TABLE IF NOT EXISTS mailbox_data (
ID TEXT KEY NOT NULL,
Email BLOB
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_data_id ON mailbox_data (ID);`,
},
{
Version: 1.1,
Description: "Create tags column",
Script: `ALTER TABLE mailbox ADD COLUMN Tags Text NOT NULL DEFAULT '[]';
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
},
{
Version: 1.2,
Description: "Creating new mailbox format",
Script: `CREATE TABLE IF NOT EXISTS mailboxtmp (
Created INTEGER NOT NULL,
ID TEXT NOT NULL,
MessageID TEXT NOT NULL,
Subject TEXT NOT NULL,
Metadata TEXT,
Size INTEGER NOT NULL,
Inline INTEGER NOT NULL,
Attachments INTEGER NOT NULL,
Read INTEGER,
Tags TEXT,
SearchText TEXT
);
INSERT INTO mailboxtmp
(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Tags)
SELECT
Sort, ID, '', json_extract(Data, '$.Subject'),Data,
json_extract(Data, '$.Size'), json_extract(Data, '$.Inline'), json_extract(Data, '$.Attachments'),
Search, Read, Tags
FROM mailbox;
DROP TABLE IF EXISTS mailbox;
ALTER TABLE mailboxtmp RENAME TO mailbox;
CREATE INDEX IF NOT EXISTS idx_created ON mailbox (Created);
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
CREATE INDEX IF NOT EXISTS idx_message_id ON mailbox (MessageID);
CREATE INDEX IF NOT EXISTS idx_subject ON mailbox (Subject);
CREATE INDEX IF NOT EXISTS idx_size ON mailbox (Size);
CREATE INDEX IF NOT EXISTS idx_inline ON mailbox (Inline);
CREATE INDEX IF NOT EXISTS idx_attachments ON mailbox (Attachments);
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
},
}
)
// DBMailSummary struct for storing mail summary
type DBMailSummary struct {
From *mail.Address
To []*mail.Address
Cc []*mail.Address
Bcc []*mail.Address
}
// InitDB will initialise the database
func InitDB() error {
p := config.DataFile
@@ -178,15 +106,6 @@ func InitDB() error {
return nil
}
// Create tables and apply migrations if required
func dbApplyMigrations() error {
driver := darwin.NewGenericDriver(db, darwin.SqliteDialect{})
d := darwin.New(driver, dbMigrations, nil)
return d.Migrate()
}
// Close will close the database, and delete if a temporary table
func Close() {
if db != nil {
@@ -243,7 +162,7 @@ func Store(body []byte) (string, error) {
searchText := createSearchText(env)
// generate unique ID
id := uuid.NewV4().String()
id := uuid.New().String()
summaryJSON, err := json.Marshal(obj)
if err != nil {
@@ -281,10 +200,11 @@ func Store(body []byte) (string, error) {
size := len(body)
inline := len(env.Inlines)
attachments := len(env.Attachments)
snippet := tools.CreateSnippet(env.Text, env.HTML)
// insert mail summary data
_, err = tx.Exec("INSERT INTO mailbox(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Tags, Read) values(?,?,?,?,?,?,?,?,?,?,0)",
created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, string(tagJSON))
_, err = tx.Exec("INSERT INTO mailbox(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Tags, Read, Snippet) values(?,?,?,?,?,?,?,?,?,?,0, ?)",
created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, string(tagJSON), snippet)
if err != nil {
return "", err
}
@@ -312,8 +232,10 @@ func Store(body []byte) (string, error) {
c.Subject = subject
c.Size = size
c.Tags = tagData
c.Snippet = snippet
websockets.Broadcast("new", c)
webhook.Send(c)
dbLastAction = time.Now()
@@ -328,7 +250,7 @@ func List(start, limit int) ([]MessageSummary, error) {
results := []MessageSummary{}
q := sqlf.From("mailbox").
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags`).
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags, Snippet`).
OrderBy("Created DESC").
Limit(limit).
Offset(start)
@@ -343,9 +265,10 @@ func List(start, limit int) ([]MessageSummary, error) {
var attachments int
var tags string
var read int
var snippet string
em := MessageSummary{}
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags); err != nil {
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &snippet); err != nil {
logger.Log().Error(err)
return
}
@@ -367,6 +290,7 @@ func List(start, limit int) ([]MessageSummary, error) {
em.Size = size
em.Attachments = attachments
em.Read = read == 1
em.Snippet = snippet
results = append(results, em)
}); err != nil {

View File

@@ -117,6 +117,38 @@ func TestRetrieveMimeEmail(t *testing.T) {
assertEqual(t, len(inlineData.Content), msg.Inline[0].Size, "inline attachment size does not match")
}
func TestMessageSummary(t *testing.T) {
setup()
defer Close()
t.Log("Testing message summary")
if _, err := Store(testMimeEmail); err != nil {
t.Log("error ", err)
t.Fail()
}
summaries, err := List(0, 1)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, len(summaries), 1, "Expected 1 result")
msg := summaries[0]
assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match")
assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match")
assertEqual(t, msg.Subject, "inline + attachment", "subject does not match")
assertEqual(t, len(msg.To), 1, "incorrect number of recipients")
assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match")
assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match")
assertEqual(t, msg.Snippet, "Message with inline image and attachment:", "\"Snippet\" does does not match")
assertEqual(t, msg.Attachments, 1, "Expected 1 attachment")
assertEqual(t, msg.MessageID, "33af2ac1-c33d-9738-35e3-a6daf90bbd89@gmail.com", "\"MessageID\" does not match")
}
func BenchmarkImportText(b *testing.B) {
setup()
defer Close()

View File

@@ -8,7 +8,7 @@ import (
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/logger"
"github.com/jhillyerd/enmime"
"github.com/leporo/sqlf"
"golang.org/x/text/language"

View File

@@ -0,0 +1,84 @@
package storage
import "github.com/GuiaBolso/darwin"
var (
dbMigrations = []darwin.Migration{
{
Version: 1.0,
Description: "Creating tables",
Script: `CREATE TABLE IF NOT EXISTS mailbox (
Sort INTEGER PRIMARY KEY AUTOINCREMENT,
ID TEXT NOT NULL,
Data BLOB,
Search TEXT,
Read INTEGER
);
CREATE INDEX IF NOT EXISTS idx_sort ON mailbox (Sort);
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
CREATE TABLE IF NOT EXISTS mailbox_data (
ID TEXT KEY NOT NULL,
Email BLOB
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_data_id ON mailbox_data (ID);`,
},
{
Version: 1.1,
Description: "Create tags column",
Script: `ALTER TABLE mailbox ADD COLUMN Tags Text NOT NULL DEFAULT '[]';
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
},
{
Version: 1.2,
Description: "Creating new mailbox format",
Script: `CREATE TABLE IF NOT EXISTS mailboxtmp (
Created INTEGER NOT NULL,
ID TEXT NOT NULL,
MessageID TEXT NOT NULL,
Subject TEXT NOT NULL,
Metadata TEXT,
Size INTEGER NOT NULL,
Inline INTEGER NOT NULL,
Attachments INTEGER NOT NULL,
Read INTEGER,
Tags TEXT,
SearchText TEXT
);
INSERT INTO mailboxtmp
(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Tags)
SELECT
Sort, ID, '', json_extract(Data, '$.Subject'),Data,
json_extract(Data, '$.Size'), json_extract(Data, '$.Inline'), json_extract(Data, '$.Attachments'),
Search, Read, Tags
FROM mailbox;
DROP TABLE IF EXISTS mailbox;
ALTER TABLE mailboxtmp RENAME TO mailbox;
CREATE INDEX IF NOT EXISTS idx_created ON mailbox (Created);
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
CREATE INDEX IF NOT EXISTS idx_message_id ON mailbox (MessageID);
CREATE INDEX IF NOT EXISTS idx_subject ON mailbox (Subject);
CREATE INDEX IF NOT EXISTS idx_size ON mailbox (Size);
CREATE INDEX IF NOT EXISTS idx_inline ON mailbox (Inline);
CREATE INDEX IF NOT EXISTS idx_attachments ON mailbox (Attachments);
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
},
{
Version: 1.3,
Description: "Create snippet column",
Script: `ALTER TABLE mailbox ADD COLUMN Snippet Text NOT NULL DEFAULT '';`,
},
}
)
// Create tables and apply migrations if required
func dbApplyMigrations() error {
driver := darwin.NewGenericDriver(db, darwin.SqliteDialect{})
d := darwin.New(driver, dbMigrations, nil)
return d.Migrate()
}

184
internal/storage/reindex.go Normal file
View File

@@ -0,0 +1,184 @@
package storage
import (
"bytes"
"context"
"database/sql"
"os"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/jhillyerd/enmime"
"github.com/leporo/sqlf"
)
// ReindexAll will regenerate the search text and snippet for a message
// and update the database.
func ReindexAll() {
ids := []string{}
var i string
chunkSize := 1000
finished := 0
err := sqlf.Select("ID").To(&i).
From("mailbox").
OrderBy("Created DESC").
QueryAndClose(nil, db, func(row *sql.Rows) {
ids = append(ids, i)
})
if err != nil {
logger.Log().Error(err)
os.Exit(1)
}
total := len(ids)
chunks := chunkBy(ids, chunkSize)
logger.Log().Infof("Reindexing %d messages", total)
// fmt.Println(len(ids), " = ", len(chunks), "chunks")
type updateStruct struct {
ID string
SearchText string
Snippet string
}
for _, ids := range chunks {
updates := []updateStruct{}
for _, id := range ids {
raw, err := GetMessageRaw(id)
if err != nil {
logger.Log().Error(err)
continue
}
r := bytes.NewReader(raw)
env, err := enmime.ReadEnvelope(r)
if err != nil {
logger.Log().Error(err)
continue
}
searchText := createSearchText(env)
snippet := tools.CreateSnippet(env.Text, env.HTML)
u := updateStruct{}
u.ID = id
u.SearchText = searchText
u.Snippet = snippet
updates = append(updates, u)
}
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
logger.Log().Error(err)
continue
}
// roll back if it fails
defer tx.Rollback()
// insert mail summary data
for _, u := range updates {
_, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ? WHERE ID = ?", u.SearchText, u.Snippet, u.ID)
if err != nil {
logger.Log().Error(err)
continue
}
}
if err := tx.Commit(); err != nil {
logger.Log().Error(err)
continue
}
finished += len(updates)
logger.Log().Printf("Reindexed: %d / %d (%d%%)", finished, total, finished*100/total)
}
}
// Reindex will regenerate the search text and snippet for a message
// and update the database.
func Reindex(id string) error {
// ids := []string{}
// var i string
// // chunkSize := 100
// err := sqlf.Select("ID").To(&i).From("mailbox_data").QueryAndClose(nil, db, func(row *sql.Rows) {
// ids = append(ids, id)
// })
// if err != nil {
// return err
// }
// chunks := chunkBy(ids, 100)
// fmt.Println(len(ids), " = ", len(chunks), "chunks")
// return nil
raw, err := GetMessageRaw(id)
if err != nil {
return err
}
r := bytes.NewReader(raw)
env, err := enmime.ReadEnvelope(r)
if err != nil {
return err
}
searchText := createSearchText(env)
snippet := tools.CreateSnippet(env.Text, env.HTML)
// return nil
// ctx := context.Background()
// tx, err := db.BeginTx(ctx, nil)
// if err != nil {
// return err
// }
// // roll back if it fails
// defer tx.Rollback()
// // insert mail summary data
// _, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ? WHERE ID = ?", searchText, snippet, id)
// if err != nil {
// return err
// }
// return tx.Commit()
_, err = sqlf.Update("mailbox").
Set("SearchText", searchText).
Set("Snippet", snippet).
Where("ID = ?", id).
ExecAndClose(context.Background(), db)
return err
}
// ctx := context.Background()
// tx, err := db.BeginTx(ctx, nil)
// if err != nil {
// return "", err
// }
func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
for chunkSize < len(items) {
items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize])
}
return append(chunks, items)
}

View File

@@ -8,8 +8,8 @@ import (
"strings"
"time"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/utils/tools"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/leporo/sqlf"
)
@@ -26,7 +26,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
limit = 50
}
q := searchParser(search)
q := searchQueryBuilder(search)
var err error
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
@@ -38,11 +38,12 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
var size int
var attachments int
var tags string
var snippet string
var read int
var ignore string
em := MessageSummary{}
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &ignore, &ignore, &ignore, &ignore); err != nil {
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil {
logger.Log().Error(err)
return
}
@@ -64,6 +65,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
em.Size = size
em.Attachments = attachments
em.Read = read == 1
em.Snippet = snippet
allResults = append(allResults, em)
}); err != nil {
@@ -95,7 +97,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
// is:read, is:unread, has:attachment, to:<term>, from:<term> & subject:<term>
// Negative searches also also included by prefixing the search term with a `-` or `!`
func DeleteSearch(search string) error {
q := searchParser(search)
q := searchQueryBuilder(search)
ids := []string{}
@@ -109,9 +111,10 @@ func DeleteSearch(search string) error {
var attachments int
var tags string
var read int
var snippet string
var ignore string
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &ignore, &ignore, &ignore, &ignore); err != nil {
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil {
logger.Log().Error(err)
return
}
@@ -187,13 +190,13 @@ func DeleteSearch(search string) error {
}
// SearchParser returns the SQL syntax for the database search based on the search arguments
func searchParser(searchString string) *sqlf.Stmt {
func searchQueryBuilder(searchString string) *sqlf.Stmt {
searchString = strings.ToLower(searchString)
// group strings with quotes as a single argument and remove quotes
args := tools.ArgsParser(searchString)
q := sqlf.From("mailbox").
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags,
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags, Snippet,
IFNULL(json_extract(Metadata, '$.To'), '{}') as ToJSON,
IFNULL(json_extract(Metadata, '$.From'), '{}') as FromJSON,
IFNULL(json_extract(Metadata, '$.Cc'), '{}') as CcJSON,

View File

@@ -89,6 +89,8 @@ type MessageSummary struct {
Size int
// Whether the message has any attachments
Attachments int
// Message snippet includes up to 250 characters
Snippet string
}
// MailboxStats struct for quick mailbox total/read lookups
@@ -98,6 +100,14 @@ type MailboxStats struct {
Tags []string
}
// DBMailSummary struct for storing mail summary
type DBMailSummary struct {
From *mail.Address
To []*mail.Address
Cc []*mail.Address
Bcc []*mail.Address
}
// AttachmentSummary returns a summary of the attachment without any binary data
func AttachmentSummary(a *enmime.Part) Attachment {
o := Attachment{}

View File

@@ -7,8 +7,8 @@ import (
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/utils/tools"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/leporo/sqlf"
)

View File

@@ -108,10 +108,9 @@ Content-Transfer-Encoding: 7bit
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
Message with inline image and attachment:<br>
<h1>Message with inline image and attachment:</h1>
<br>
<img src="cid:part1.845LaYlX.wtWMpWwa@gmail.com"
moz-do-not-send="false"><br>
<p><img src="cid:part1.845LaYlX.wtWMpWwa@gmail.com"></p>
<br>
<br>
</body>

View File

@@ -6,7 +6,7 @@ import (
"testing"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/logger"
)
var (

View File

@@ -10,10 +10,10 @@ import (
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/html2text"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/server/websockets"
"github.com/axllent/mailpit/utils/logger"
"github.com/jhillyerd/enmime"
"github.com/k3a/html2text"
"github.com/leporo/sqlf"
)
@@ -39,12 +39,8 @@ func createSearchText(env *enmime.Envelope) string {
b.WriteString(env.GetHeader("Bcc") + " ")
b.WriteString(env.GetHeader("Reply-To") + " ")
b.WriteString(env.GetHeader("Return-Path") + " ")
h := strings.TrimSpace(
html2text.HTML2TextWithOptions(
env.HTML,
html2text.WithLinksInnerText(),
),
)
h := html2text.Strip(env.HTML, true)
if h != "" {
b.WriteString(h + " ")
} else {
@@ -62,6 +58,9 @@ func createSearchText(env *enmime.Envelope) string {
// CleanString removes unwanted characters from stored search text and search queries
func cleanString(str string) string {
// replace \uFEFF with space, see https://github.com/golang/go/issues/42274#issuecomment-1017258184
str = strings.ReplaceAll(str, string('\uFEFF'), " ")
// remove/replace new lines
re := regexp.MustCompile(`(\r?\n|\t|>|<|"|\,|;|\(|\))`)
str = re.ReplaceAllString(str, " ")

99
internal/tools/message.go Normal file
View File

@@ -0,0 +1,99 @@
// Package tools provides various methods for various things
package tools
import (
"bufio"
"bytes"
"net/mail"
"regexp"
"github.com/axllent/mailpit/internal/logger"
)
// RemoveMessageHeaders scans a message for headers, if found them removes them.
// It will only remove a single instance of any given message header.
func RemoveMessageHeaders(msg []byte, headers []string) ([]byte, error) {
reader := bytes.NewReader(msg)
m, err := mail.ReadMessage(reader)
if err != nil {
return nil, err
}
reBlank := regexp.MustCompile(`^\s+`)
for _, hdr := range headers {
// case-insensitive
reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta(hdr+":"))
// header := []byte(hdr + ":")
if m.Header.Get(hdr) != "" {
scanner := bufio.NewScanner(bytes.NewReader(msg))
found := false
hdr := []byte("")
for scanner.Scan() {
line := scanner.Bytes()
if !found && reHdr.Match(line) {
// add the first line starting with <header>:
hdr = append(hdr, line...)
hdr = append(hdr, []byte("\r\n")...)
found = true
} else if found && reBlank.Match(line) {
// add any following lines starting with a whitespace (tab or space)
hdr = append(hdr, line...)
hdr = append(hdr, []byte("\r\n")...)
} else if found {
// stop scanning, we have the full <header>
break
}
}
if len(hdr) > 0 {
logger.Log().Debugf("[release] removed %s header", hdr)
msg = bytes.Replace(msg, hdr, []byte(""), 1)
}
}
}
return msg, nil
}
// UpdateMessageHeader scans a message for a header and updates its value if found.
func UpdateMessageHeader(msg []byte, header, value string) ([]byte, error) {
reader := bytes.NewReader(msg)
m, err := mail.ReadMessage(reader)
if err != nil {
return nil, err
}
if m.Header.Get(header) != "" {
reBlank := regexp.MustCompile(`^\s+`)
reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta(header+":"))
scanner := bufio.NewScanner(bytes.NewReader(msg))
found := false
hdr := []byte("")
for scanner.Scan() {
line := scanner.Bytes()
if !found && reHdr.Match(line) {
// add the first line starting with <header>:
hdr = append(hdr, line...)
hdr = append(hdr, []byte("\r\n")...)
found = true
} else if found && reBlank.Match(line) {
// add any following lines starting with a whitespace (tab or space)
hdr = append(hdr, line...)
hdr = append(hdr, []byte("\r\n")...)
} else if found {
// stop scanning, we have the full <header>
break
}
}
if len(hdr) > 0 {
logger.Log().Debugf("[release] replaced %s header", hdr)
msg = bytes.Replace(msg, hdr, []byte(header+": "+value+"\r\n"), 1)
}
}
return msg, nil
}

View File

@@ -0,0 +1,44 @@
package tools
import (
"regexp"
"strings"
"github.com/axllent/mailpit/internal/html2text"
)
// CreateSnippet returns a message snippet. It will use the HTML version (if it exists)
// otherwise the text version.
func CreateSnippet(text, html string) string {
text = strings.TrimSpace(text)
html = strings.TrimSpace(html)
limit := 200
spaceRe := regexp.MustCompile(`\s+`)
if text == "" && html == "" {
return ""
}
if html != "" {
data := html2text.Strip(html, false)
if len(data) <= limit {
return data
}
return data[0:limit] + "..."
}
if text != "" {
// replace \uFEFF with space, see https://github.com/golang/go/issues/42274#issuecomment-1017258184
text = strings.ReplaceAll(text, string('\uFEFF'), " ")
text = strings.TrimSpace(spaceRe.ReplaceAllString(text, " "))
if len(text) <= limit {
return text
}
return text[0:limit] + "..."
}
return ""
}

View File

@@ -0,0 +1,71 @@
package tools
import (
"reflect"
"testing"
)
func TestArgsParser(t *testing.T) {
tests := map[string][]string{}
tests["this is a test"] = []string{"this", "is", "a", "test"}
tests["\"this is\" a test"] = []string{"this is", "a", "test"}
tests["!\"this is\" a test"] = []string{"!this is", "a", "test"}
tests["subject:this is a test"] = []string{"subject:this", "is", "a", "test"}
tests["subject:\"this is\" a test"] = []string{"subject:this is", "a", "test"}
tests["subject:\"this is\" \"a test\""] = []string{"subject:this is", "a test"}
tests["subject:\"this 'is\" \"a test\""] = []string{"subject:this 'is", "a test"}
tests["subject:\"this 'is a test"] = []string{"subject:this 'is a test"}
tests["\"this is a test\"=\"this is a test\""] = []string{"this is a test=this is a test"}
for search, expected := range tests {
res := ArgsParser(search)
if !reflect.DeepEqual(res, expected) {
t.Log("Args parser error:", res, "!=", expected)
t.Fail()
}
}
}
func TestCleanTag(t *testing.T) {
tests := map[string]string{}
tests["this is a test"] = "this is a test"
tests["thiS IS a Test"] = "thiS IS a Test"
tests["thiS IS a Test :-)"] = "thiS IS a Test -"
tests[" thiS 99 IS a Test :-)"] = "thiS 99 IS a Test -"
tests["this_is-a test "] = "this_is-a test"
tests["this_is-a&^%%(*)@ test"] = "this_is-a test"
for search, expected := range tests {
res := CleanTag(search)
if res != expected {
t.Log("CleanTags error:", res, "!=", expected)
t.Fail()
}
}
}
func TestSnippets(t *testing.T) {
tests := map[string]string{}
tests["this is a test"] = "this is a test"
tests["thiS IS a Test"] = "thiS IS a Test"
tests["thiS IS a Test :-)"] = "thiS IS a Test :-)"
tests["<h1>This is a test.</h1> "] = "This is a test."
tests["this_is-a test "] = "this_is-a test"
tests["this_is-a&^%%(*)@ test"] = "this_is-a&^%%(*)@ test"
tests["<h1>Heading</h1><p>Paragraph</p>"] = "Heading Paragraph"
tests[`<h1>Heading</h1>
<p>Paragraph</p>`] = "Heading Paragraph"
tests[`<h1>Heading</h1><p> <a href="https://github.com">linked text</a></p>`] = "Heading linked text"
// broken html
tests[`<h1>Heading</h3><p> <a href="https://github.com">linked text.`] = "Heading linked text."
// truncation to 200 chars + ...
tests["abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789"] = "abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmno..."
for str, expected := range tests {
res := CreateSnippet(str, str)
if res != expected {
t.Log("CreateSnippet error:", res, "!=", expected)
t.Fail()
}
}
}

View File

@@ -1,3 +1,4 @@
// package Updater checks and downloads new versions
package updater
import (
@@ -6,14 +7,15 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/semver"
)
@@ -49,13 +51,27 @@ type Release struct {
func GithubLatest(repo, name string) (string, string, string, error) {
releaseURL := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo)
resp, err := http.Get(releaseURL) // #nosec
timeout := time.Duration(5 * time.Second)
client := http.Client{
Timeout: timeout,
}
req, err := http.NewRequest("GET", releaseURL, nil)
if err != nil {
return "", "", "", err
}
req.Header.Set("User-Agent", "Mailpit/"+config.Version)
resp, err := client.Do(req)
if err != nil {
return "", "", "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", "", err

1040
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ import (
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/logger"
"github.com/reiver/go-telnet"
flag "github.com/spf13/pflag"
)

View File

@@ -9,16 +9,17 @@ import (
"net/mail"
"strconv"
"strings"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/htmlcheck"
"github.com/axllent/mailpit/internal/linkcheck"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server/smtpd"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/htmlcheck"
"github.com/axllent/mailpit/utils/linkcheck"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/utils/tools"
"github.com/google/uuid"
"github.com/gorilla/mux"
uuid "github.com/satori/go.uuid"
)
// GetMessages returns a paginated list of messages as JSON
@@ -144,11 +145,11 @@ func Search(w http.ResponseWriter, r *http.Request) {
// DeleteSearch will delete all messages matching a search
func DeleteSearch(w http.ResponseWriter, r *http.Request) {
// swagger:route DELETE /api/v1/search messages MessagesSummary
// swagger:route DELETE /api/v1/search messages DeleteSearch
//
// # Delete messages by search
//
// Deletes messages matching a search.
// Delete all messages matching a search.
//
// Produces:
// - application/json
@@ -196,7 +197,7 @@ func GetMessage(w http.ResponseWriter, r *http.Request) {
// Parameters:
// + name: ID
// in: path
// description: Database ID
// description: Message database ID
// required: true
// type: string
//
@@ -237,7 +238,7 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
// Parameters:
// + name: ID
// in: path
// description: Database ID
// description: Message database ID
// required: true
// type: string
// + name: PartID
@@ -362,11 +363,11 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
// DeleteMessages (method: DELETE) deletes all messages matching IDS.
func DeleteMessages(w http.ResponseWriter, r *http.Request) {
// swagger:route DELETE /api/v1/messages messages Delete
// swagger:route DELETE /api/v1/messages messages DeleteMessages
//
// # Delete messages
//
// If no IDs are provided then all messages are deleted.
// Delete individual or all messages. If no IDs are provided then all messages are deleted.
//
// Consumes:
// - application/json
@@ -376,13 +377,6 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
//
// Schemes: http, https
//
// Parameters:
// + name: ids
// in: body
// description: Database IDs to delete
// required: false
// type: DeleteRequest
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
@@ -406,7 +400,7 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
}
}
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Content-Type", "application/plain")
_, _ = w.Write([]byte("ok"))
}
@@ -427,13 +421,6 @@ func SetReadStatus(w http.ResponseWriter, r *http.Request) {
//
// Schemes: http, https
//
// Parameters:
// + name: ids
// in: body
// description: Database IDs to update
// required: false
// type: SetReadStatusRequest
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
@@ -491,7 +478,7 @@ func SetReadStatus(w http.ResponseWriter, r *http.Request) {
// GetTags (method: GET) will get all tags currently in use
func GetTags(w http.ResponseWriter, _ *http.Request) {
// swagger:route GET /api/v1/tags tags SetTags
// swagger:route GET /api/v1/tags tags GetTags
//
// # Get all current tags
//
@@ -524,7 +511,7 @@ func SetTags(w http.ResponseWriter, r *http.Request) {
//
// # Set message tags
//
// To remove all tags from a message, pass an empty tags array.
// This will overwrite any existing tags for selected message database IDs. To remove all tags from a message, pass an empty tags array.
//
// Consumes:
// - application/json
@@ -534,13 +521,6 @@ func SetTags(w http.ResponseWriter, r *http.Request) {
//
// Schemes: http, https
//
// Parameters:
// + name: ids
// in: body
// description: Database IDs to update
// required: true
// type: SetTagsRequest
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
@@ -576,11 +556,11 @@ func SetTags(w http.ResponseWriter, r *http.Request) {
// ReleaseMessage (method: POST) will release a message via a pre-configured external SMTP server.
// If no IDs are provided then all messages are updated.
func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
// swagger:route POST /api/v1/message/{ID}/release message Release
// swagger:route POST /api/v1/message/{ID}/release message ReleaseMessage
//
// # Release message
//
// Release a message via a pre-configured external SMTP server..
// Release a message via a pre-configured external SMTP server. This is only enabled if message relaying has been configured.
//
// Consumes:
// - application/json
@@ -590,18 +570,6 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: Database ID
// required: true
// type: string
// + name: to
// in: body
// description: Array of email addresses to release message to
// required: true
// type: ReleaseMessageRequest
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
@@ -618,7 +586,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
data := releaseMessageRequest{}
data := releaseMessageRequestBody{}
if err := decoder.Decode(&data); err != nil {
httpError(w, err.Error())
@@ -665,7 +633,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
from = senders[0].Address
}
msg, err = tools.RemoveMessageHeaders(msg, []string{"Bcc", "Message-Id"})
msg, err = tools.RemoveMessageHeaders(msg, []string{"Bcc"})
if err != nil {
httpError(w, err.Error())
return
@@ -685,10 +653,21 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
from = config.SMTPRelayConfig.ReturnPath
}
// update message date
msg, err = tools.UpdateMessageHeader(msg, "Date", time.Now().Format(time.RFC1123Z))
if err != nil {
httpError(w, err.Error())
return
}
// generate unique ID
uid := uuid.NewV4().String() + "@mailpit"
// add unique ID
msg = append([]byte("Message-Id: <"+uid+">\r\n"), msg...)
uid := uuid.New().String() + "@mailpit"
// update Message-Id with unique ID
msg, err = tools.UpdateMessageHeader(msg, "Message-Id", "<"+uid+">")
if err != nil {
httpError(w, err.Error())
return
}
if err := smtpd.Send(from, tos, msg); err != nil {
logger.Log().Errorf("[smtp] error sending message: %s", err.Error())
@@ -702,7 +681,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
// HTMLCheck returns a summary of the HTML client support
func HTMLCheck(w http.ResponseWriter, r *http.Request) {
// swagger:route GET /api/v1/message/{ID}/html-check Other HTMLCheckResponse
// swagger:route GET /api/v1/message/{ID}/html-check Other HTMLCheck
//
// # HTML check (beta)
//
@@ -716,13 +695,6 @@ func HTMLCheck(w http.ResponseWriter, r *http.Request) {
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: Database ID
// required: true
// type: string
//
// Responses:
// 200: HTMLCheckResponse
// default: ErrorResponse
@@ -754,7 +726,7 @@ func HTMLCheck(w http.ResponseWriter, r *http.Request) {
// LinkCheck returns a summary of links in the email
func LinkCheck(w http.ResponseWriter, r *http.Request) {
// swagger:route GET /api/v1/message/{ID}/link-check Other LinkCheckResponse
// swagger:route GET /api/v1/message/{ID}/link-check Other LinkCheck
//
// # Link check (beta)
//
@@ -768,19 +740,6 @@ func LinkCheck(w http.ResponseWriter, r *http.Request) {
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: Database ID
// required: true
// type: string
// + name: follow
// in: query
// description: Follow redirects
// required: false
// type: boolean
// default: false
//
// Responses:
// 200: LinkCheckResponse
// default: ErrorResponse

View File

@@ -7,8 +7,8 @@ import (
"runtime"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/updater"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/updater"
)
// Response includes the current and latest Mailpit version, database info, and memory usage
@@ -30,7 +30,7 @@ type appInformation struct {
}
// AppInfo returns some basic details about the running app, and latest release.
func AppInfo(w http.ResponseWriter, r *http.Request) {
func AppInfo(w http.ResponseWriter, _ *http.Request) {
// swagger:route GET /api/v1/info application AppInformation
//
// # Get application information

View File

@@ -1,9 +1,9 @@
package apiv1
import (
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/htmlcheck"
"github.com/axllent/mailpit/utils/linkcheck"
"github.com/axllent/mailpit/internal/htmlcheck"
"github.com/axllent/mailpit/internal/linkcheck"
"github.com/axllent/mailpit/internal/storage"
)
// MessagesSummary is a summary of a list of messages
@@ -22,9 +22,6 @@ type MessagesSummary struct {
// Total number of messages matching current query
MessagesCount int `json:"messages_count"`
// // Number of results returned on current page
// Count int `json:"count"`
// Pagination offset
Start int `json:"start"`
@@ -32,7 +29,7 @@ type MessagesSummary struct {
Tags []string `json:"tags"`
// Messages summary
// in:body
// in: body
Messages []storage.MessageSummary `json:"messages"`
}

View File

@@ -6,6 +6,8 @@ package apiv1
// swagger:response InfoResponse
type infoResponse struct {
// Application information
//
// in: body
Body appInformation
}
@@ -13,6 +15,8 @@ type infoResponse struct {
// swagger:response WebUIConfigurationResponse
type webUIConfigurationResponse struct {
// Web UI configuration settings
//
// in: body
Body webUIConfiguration
}
@@ -28,74 +32,137 @@ type messagesSummaryResponse struct {
// swagger:model MessageHeaders
type messageHeaders map[string][]string
// swagger:parameters DeleteMessages
type deleteMessagesParams struct {
// in: body
Body *deleteMessagesRequestBody
}
// Delete request
// swagger:model DeleteRequest
type deleteRequest struct {
// ids
// in:body
type deleteMessagesRequestBody struct {
// Array of message database IDs
//
// required: false
// example: ["5dec4247-812e-4b77-9101-e25ad406e9ea", "8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"]
IDs []string `json:"ids"`
}
// swagger:parameters SetReadStatus
type setReadStatusParams struct {
// in: body
Body *setReadStatusRequestBody
}
// Set read status request
// swagger:model SetReadStatusRequest
type setReadStatusRequest struct {
// swagger:model setReadStatusRequestBody
type setReadStatusRequestBody struct {
// Read status
//
// required: false
// default: false
// example: true
Read bool `json:"read"`
// ids
// in:body
// Array of message database IDs
//
// required: false
// example: ["5dec4247-812e-4b77-9101-e25ad406e9ea", "8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"]
IDs []string `json:"ids"`
}
// swagger:parameters SetTags
type setTagsParams struct {
// in: body
Body *setTagsRequestBody
}
// Set tags request
// swagger:model SetTagsRequest
type setTagsRequest struct {
// Tags
// in:body
// swagger:model setTagsRequestBody
type setTagsRequestBody struct {
// Array of tag names to set
//
// required: true
// example: ["Tag 1", "Tag 2"]
Tags []string `json:"tags"`
// IDs
// in:body
// Array of message database IDs
//
// required: true
// example: ["5dec4247-812e-4b77-9101-e25ad406e9ea", "8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"]
IDs []string `json:"ids"`
}
// swagger:parameters ReleaseMessage
type releaseMessageParams struct {
// Message database ID
//
// in: path
// description: Message database ID
// required: true
ID string
// in: body
Body *releaseMessageRequestBody
}
// Release request
// swagger:model ReleaseMessageRequest
type releaseMessageRequest struct {
// To
// in:body
// swagger:model releaseMessageRequestBody
type releaseMessageRequestBody struct {
// Array of email addresses to relay the message to
//
// required: true
// example: ["user1@example.com", "user2@example.com"]
To []string `json:"to"`
}
// swagger:parameters HTMLCheck
type htmlCheckParams struct {
// Message database ID
//
// in: path
// description: Message database ID
// required: true
ID string
}
// swagger:parameters LinkCheck
type linkCheckParams struct {
// Message database ID
//
// in: path
// description: Message database ID
// required: true
ID string
// Follow redirects
//
// in: query
// description: Follow redirects
// required: false
// default: false
Follow string `json:"follow"`
}
// Binary data response inherits the attachment's content type
// swagger:response BinaryResponse
type binaryResponse struct {
// in: body
Body string
}
type binaryResponse string
// Plain text response
// swagger:response TextResponse
type textResponse struct {
// in: body
Body string
}
type textResponse string
// Error response
// HTML response
// swagger:response HTMLResponse
type htmlResponse string
// HTTP error response will return with a >= 400 response code
// swagger:response ErrorResponse
type errorResponse struct {
// The error message
// in: body
Body string
}
type errorResponse string
// Plain text "ok" response
// swagger:response OKResponse
type okResponse struct {
// Default response
// in: body
Body string
}
type okResponse string
// Plain JSON array response
// swagger:response ArrayResponse

View File

@@ -10,8 +10,8 @@ import (
"net/http"
"strings"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/disintegration/imaging"
"github.com/gorilla/mux"
"github.com/jhillyerd/enmime"

221
server/handlers/messages.go Normal file
View File

@@ -0,0 +1,221 @@
package handlers
import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/storage"
"github.com/gorilla/mux"
)
// RedirectToLatestMessage (method: GET) redirects the web UI to the latest message
func RedirectToLatestMessage(w http.ResponseWriter, r *http.Request) {
messages := []storage.MessageSummary{}
var err error
search := strings.TrimSpace(r.URL.Query().Get("query"))
if search != "" {
messages, _, err = storage.Search(search, 0, 1)
if err != nil {
httpError(w, err.Error())
return
}
} else {
messages, err = storage.List(0, 1)
if err != nil {
httpError(w, err.Error())
return
}
}
uri := config.Webroot
if len(messages) == 1 {
uri, err = url.JoinPath(uri, "/view/"+messages[0].ID)
if err != nil {
httpError(w, err.Error())
return
}
}
http.Redirect(w, r, uri, 302)
}
// GetMessageHTML (method: GET) returns a rendered version of a message's HTML part
func GetMessageHTML(w http.ResponseWriter, r *http.Request) {
// swagger:route GET /view/{ID}.html testing GetMessageHTML
//
// # Render message HTML part
//
// Renders just the message's HTML part which can be used for UI integration testing.
// Attached inline images are modified to link to the API provided they exist.
// Note that is the message does not contain a HTML part then an 404 error is returned.
//
// The ID can be set to `latest` to return the latest message.
//
// Produces:
// - text/html
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: Database ID or latest
// required: true
// type: string
//
// Responses:
// 200: HTMLResponse
// default: ErrorResponse
vars := mux.Vars(r)
id := vars["id"]
if id == "latest" {
messages := []storage.MessageSummary{}
var err error
search := strings.TrimSpace(r.URL.Query().Get("query"))
if search != "" {
messages, _, err = storage.Search(search, 0, 1)
if err != nil {
httpError(w, err.Error())
return
}
} else {
messages, err = storage.List(0, 1)
if err != nil {
httpError(w, err.Error())
return
}
}
if len(messages) == 0 {
w.WriteHeader(404)
fmt.Fprint(w, "Message not found")
return
}
id = messages[0].ID
}
msg, err := storage.GetMessage(id)
if err != nil {
w.WriteHeader(404)
fmt.Fprint(w, "Message not found")
return
}
if msg.HTML == "" {
w.WriteHeader(404)
fmt.Fprint(w, "This message does not contain a HTML part")
return
}
html := linkInlineImages(msg)
w.Header().Add("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(html))
}
// GetMessageText (method: GET) returns a message's text part
func GetMessageText(w http.ResponseWriter, r *http.Request) {
// swagger:route GET /view/{ID}.txt testing GetMessageText
//
// # Render message text part
//
// Renders just the message's text part which can be used for UI integration testing.
//
// The ID can be set to `latest` to return the latest message.
//
// Produces:
// - text/plain
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: Database ID or latest
// required: true
// type: string
//
// Responses:
// 200: TextResponse
// default: ErrorResponse
vars := mux.Vars(r)
id := vars["id"]
if id == "latest" {
messages := []storage.MessageSummary{}
var err error
search := strings.TrimSpace(r.URL.Query().Get("query"))
if search != "" {
messages, _, err = storage.Search(search, 0, 1)
if err != nil {
httpError(w, err.Error())
return
}
} else {
messages, err = storage.List(0, 1)
if err != nil {
httpError(w, err.Error())
return
}
}
if len(messages) == 0 {
w.WriteHeader(404)
fmt.Fprint(w, "Message not found")
return
}
id = messages[0].ID
}
msg, err := storage.GetMessage(id)
if err != nil {
w.WriteHeader(404)
fmt.Fprint(w, "Message not found")
return
}
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte(msg.Text))
}
// This will rewrite all inline image paths to API URLs
func linkInlineImages(msg *storage.Message) string {
html := msg.HTML
for _, a := range msg.Inline {
if a.ContentID != "" {
re := regexp.MustCompile(`(?i)(=["\']?)(cid:` + regexp.QuoteMeta(a.ContentID) + `)(["|\'|\\s|\\/|>|;])`)
u := config.Webroot + "api/v1/message/" + msg.ID + "/part/" + a.PartID
matches := re.FindAllStringSubmatch(html, -1)
for _, m := range matches {
html = strings.ReplaceAll(html, m[0], m[1]+u+m[3])
}
}
}
for _, a := range msg.Attachments {
if a.ContentID != "" {
re := regexp.MustCompile(`(?i)(=["\']?)(cid:` + regexp.QuoteMeta(a.ContentID) + `)(["|\'|\\s|\\/|>|;])`)
u := config.Webroot + "api/v1/message/" + msg.ID + "/part/" + a.PartID
matches := re.FindAllStringSubmatch(html, -1)
for _, m := range matches {
html = strings.ReplaceAll(html, m[0], m[1]+u+m[3])
}
}
}
return html
}

View File

@@ -2,6 +2,7 @@
package handlers
import (
"crypto/tls"
"fmt"
"io"
"net/http"
@@ -11,7 +12,7 @@ import (
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/logger"
)
var linkRe = regexp.MustCompile(`(?i)^https?:\/\/`)
@@ -31,8 +32,15 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
return
}
tr := &http.Transport{}
if config.AllowUntrustedTLS {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
client := &http.Client{
Timeout: 10 * time.Second,
Transport: tr,
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("GET", uri, nil)

View File

@@ -15,11 +15,12 @@ import (
"text/template"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/server/apiv1"
"github.com/axllent/mailpit/server/handlers"
"github.com/axllent/mailpit/server/websockets"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/gorilla/mux"
)
@@ -67,24 +68,35 @@ func Listen() {
r.HandleFunc(redirect, middleWareFunc(addSlashToWebroot)).Methods("GET")
}
// handle everything else with the virtual index.html
r.PathPrefix(config.Webroot).Handler(middleWareFunc(index)).Methods("GET")
// UI shortcut
r.HandleFunc(config.Webroot+"view/latest", handlers.RedirectToLatestMessage).Methods("GET")
// frontend testing
r.HandleFunc(config.Webroot+"view/{id}.html", handlers.GetMessageHTML).Methods("GET")
r.HandleFunc(config.Webroot+"view/{id}.txt", handlers.GetMessageText).Methods("GET")
// web UI via virtual index.html
r.PathPrefix(config.Webroot + "view/").Handler(middleWareFunc(index)).Methods("GET")
r.Path(config.Webroot + "search").Handler(middleWareFunc(index)).Methods("GET")
r.Path(config.Webroot).Handler(middleWareFunc(index)).Methods("GET")
// put it all together
http.Handle("/", r)
if config.UIAuthFile != "" {
logger.Log().Info("[http] enabling web UI basic authentication")
if auth.UICredentials != nil {
logger.Log().Info("[http] enabling basic authentication")
}
// Mark the application here as ready
isReady.Store(true)
logger.Log().Infof("[http] starting on %s", config.HTTPListen)
if config.UITLSCert != "" && config.UITLSKey != "" {
logger.Log().Infof("[http] starting secure server on https://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
logger.Log().Infof("[http] accessible via https://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UITLSCert, config.UITLSKey, nil))
} else {
logger.Log().Infof("[http] starting server on http://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
logger.Log().Infof("[http] accessible via http://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil))
}
}
@@ -152,7 +164,7 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
w.Header().Set("Access-Control-Allow-Headers", "*")
}
if config.UIAuthFile != "" {
if auth.UICredentials != nil {
user, pass, ok := r.BasicAuth()
if !ok {
@@ -160,7 +172,21 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
return
}
if !config.UIAuth.Match(user, pass) {
if !auth.UICredentials.Match(user, pass) {
basicAuthResponse(w)
return
}
}
if auth.UICredentials != nil {
user, pass, ok := r.BasicAuth()
if !ok {
basicAuthResponse(w)
return
}
if !auth.UICredentials.Match(user, pass) {
basicAuthResponse(w)
return
}
@@ -191,7 +217,7 @@ func middlewareHandler(h http.Handler) http.Handler {
w.Header().Set("Access-Control-Allow-Headers", "*")
}
if config.UIAuthFile != "" {
if auth.UICredentials != nil {
user, pass, ok := r.BasicAuth()
if !ok {
@@ -199,7 +225,7 @@ func middlewareHandler(h http.Handler) http.Handler {
return
}
if !config.UIAuth.Match(user, pass) {
if !auth.UICredentials.Match(user, pass) {
basicAuthResponse(w)
return
}
@@ -293,10 +319,6 @@ func index(w http.ResponseWriter, _ *http.Request) {
buff.Bytes()
// f, err := embeddedFS.ReadFile("public/index.html")
// if err != nil {
// panic(err)
// }
w.Header().Add("Content-Type", "text/html")
_, _ = w.Write(buff.Bytes())
}

View File

@@ -12,9 +12,9 @@ import (
"testing"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/server/apiv1"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/jhillyerd/enmime"
)
@@ -25,7 +25,7 @@ var (
}
)
func Test_APIv1(t *testing.T) {
func TestAPIv1Messages(t *testing.T) {
setup()
defer storage.Close()
@@ -54,7 +54,7 @@ func Test_APIv1(t *testing.T) {
t.Errorf(err.Error())
}
// read first 10
// read first 10 messages
t.Log("Read first 10 messages including raw & headers")
putIDS := []string{}
for idx, msg := range m.Messages {
@@ -66,12 +66,12 @@ func Test_APIv1(t *testing.T) {
t.Errorf(err.Error())
}
// test RAW
// get RAW
if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID + "/raw"); err != nil {
t.Errorf(err.Error())
}
// test headers
// het headers
if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID + "/headers"); err != nil {
t.Errorf(err.Error())
}
@@ -79,11 +79,63 @@ func Test_APIv1(t *testing.T) {
// store for later
putIDS = append(putIDS, msg.ID)
}
// 10 should be marked as read
assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100)
// delete all
t.Log("Delete all messages")
_, err = clientDelete(ts.URL+"/api/v1/messages", "{}")
if err != nil {
t.Errorf("Expected nil, received %s", err.Error())
}
assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0)
}
func TestAPIv1ToggleReadStatus(t *testing.T) {
setup()
defer storage.Close()
r := apiRoutes()
ts := httptest.NewServer(r)
defer ts.Close()
m, err := fetchMessages(ts.URL + "/api/v1/messages")
if err != nil {
t.Errorf(err.Error())
}
// check count of empty database
assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0)
// insert 100
t.Log("Insert 100 messages")
insertEmailData(t)
assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)
m, err = fetchMessages(ts.URL + "/api/v1/messages")
if err != nil {
t.Errorf(err.Error())
}
// read first 10 IDs
t.Log("Get first 10 IDs")
putIDS := []string{}
for idx, msg := range m.Messages {
if idx == 10 {
break
}
// store for later
putIDS = append(putIDS, msg.ID)
}
assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)
// mark first 10 as unread
t.Log("Mark first 10 as unread")
t.Log("Mark first 10 as read")
putData := putDataStruct
putData.Read = true
putData.IDs = putIDS
j, err := json.Marshal(putData)
if err != nil {
@@ -93,11 +145,11 @@ func Test_APIv1(t *testing.T) {
if err != nil {
t.Errorf(err.Error())
}
assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)
assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100)
// mark first 10 as read
t.Log("Mark first 10 as read")
putData.Read = true
t.Log("Mark first 10 as unread")
putData.Read = false
j, err = json.Marshal(putData)
if err != nil {
t.Errorf(err.Error())
@@ -106,25 +158,7 @@ func Test_APIv1(t *testing.T) {
if err != nil {
t.Errorf(err.Error())
}
assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100)
// search
assertSearchEqual(t, ts.URL+"/api/v1/search", "from-1@example.com", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "to:from-1@example.com", 0)
assertSearchEqual(t, ts.URL+"/api/v1/search", "from:@example.com", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line\"", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line 17 end\"", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "!thisdoesnotexist", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "-thisdoesnotexist", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "thisdoesnotexist", 0)
// delete first 10
t.Log("Delete first 10")
_, err = clientDelete(ts.URL+"/api/v1/messages", string(j))
if err != nil {
t.Errorf(err.Error())
}
assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 90)
assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)
// mark all as read
putData.Read = true
@@ -139,15 +173,34 @@ func Test_APIv1(t *testing.T) {
if err != nil {
t.Errorf(err.Error())
}
assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 90)
assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 100)
}
// delete all
t.Log("Delete all messages")
_, err = clientDelete(ts.URL+"/api/v1/messages", "{}")
if err != nil {
t.Errorf("Expected nil, received %s", err.Error())
}
assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0)
func TestAPIv1Search(t *testing.T) {
setup()
defer storage.Close()
r := apiRoutes()
ts := httptest.NewServer(r)
defer ts.Close()
// insert 100
t.Log("Insert 100 messages")
insertEmailData(t)
assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100)
// search
assertSearchEqual(t, ts.URL+"/api/v1/search", "from-1@example.com", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "from:from-1@example.com", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "-from:from-1@example.com", 99)
assertSearchEqual(t, ts.URL+"/api/v1/search", "to:from-1@example.com", 0)
assertSearchEqual(t, ts.URL+"/api/v1/search", "from:@example.com", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line\"", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line 17 end\"", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "!thisdoesnotexist", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "-thisdoesnotexist", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "thisdoesnotexist", 0)
}
func setup() {

View File

@@ -8,7 +8,7 @@ import (
"net/smtp"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/logger"
)
func allowedRecipients(to []string) []string {
@@ -63,22 +63,10 @@ func Send(from string, to []string, msg []byte) error {
}
}
var a smtp.Auth
auth := relayAuthFromConfig()
if config.SMTPRelayConfig.Auth == "plain" {
a = smtp.PlainAuth("", config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password, config.SMTPRelayConfig.Host)
}
if config.SMTPRelayConfig.Auth == "login" {
a = LoginAuth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password)
}
if config.SMTPRelayConfig.Auth == "cram-md5" {
a = smtp.CRAMMD5Auth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Secret)
}
if a != nil {
if err = c.Auth(a); err != nil {
if auth != nil {
if err = c.Auth(auth); err != nil {
return fmt.Errorf("error response to AUTH command: %s", err.Error())
}
}
@@ -109,6 +97,25 @@ func Send(from string, to []string, msg []byte) error {
return c.Quit()
}
// Return the SMTP relay authentication based on config
func relayAuthFromConfig() smtp.Auth {
var a smtp.Auth
if config.SMTPRelayConfig.Auth == "plain" {
a = smtp.PlainAuth("", config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password, config.SMTPRelayConfig.Host)
}
if config.SMTPRelayConfig.Auth == "login" {
a = LoginAuth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password)
}
if config.SMTPRelayConfig.Auth == "cram-md5" {
a = smtp.CRAMMD5Auth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Secret)
}
return a
}
// Custom implementation of LOGIN SMTP authentication
// @see https://gist.github.com/andelf/5118732
type loginAuth struct {
@@ -120,7 +127,7 @@ func LoginAuth(username, password string) smtp.Auth {
return &loginAuth{username, password}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", []byte{}, nil
}

View File

@@ -10,10 +10,11 @@ import (
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/google/uuid"
"github.com/mhale/smtpd"
uuid "github.com/satori/go.uuid"
)
func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
@@ -56,7 +57,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
// add a message ID if not set
if messageID == "" {
// generate unique ID
messageID = uuid.NewV4().String() + "@mailpit"
messageID = uuid.New().String() + "@mailpit"
// add unique ID
data = append([]byte("Message-Id: <"+messageID+">\r\n"), data...)
} else if config.IgnoreDuplicateIDs {
@@ -129,7 +130,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
}
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, _ []byte) (bool, error) {
allow := config.SMTPAuthConfig.Match(string(username), string(password))
allow := auth.SMTPCredentials.Match(string(username), string(password))
if allow {
logger.Log().Debugf("[smtpd] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
} else {
@@ -149,20 +150,20 @@ func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, _ []
// Listen starts the SMTPD server
func Listen() error {
if config.SMTPAuthAllowInsecure {
if config.SMTPAuthFile != "" {
logger.Log().Infof("[smtpd] enabling login auth via %s (insecure)", config.SMTPAuthFile)
if auth.SMTPCredentials != nil {
logger.Log().Info("[smtpd] enabling login auth (insecure)")
} else if config.SMTPAuthAcceptAny {
logger.Log().Info("[smtpd] enabling all auth (insecure)")
}
} else {
if config.SMTPAuthFile != "" {
logger.Log().Infof("[smtpd] enabling login auth via %s (TLS)", config.SMTPAuthFile)
if auth.SMTPCredentials != nil {
logger.Log().Info("[smtpd] enabling login auth (TLS)")
} else if config.SMTPAuthAcceptAny {
logger.Log().Info("[smtpd] enabling any auth (TLS)")
}
}
logger.Log().Infof("[smtpd] starting on %s", logger.CleanIP(config.SMTPListen))
logger.Log().Infof("[smtpd] starting on %s", config.SMTPListen)
return listenAndServe(config.SMTPListen, mailHandler, authHandler)
}
@@ -181,7 +182,7 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
}
if config.SMTPAuthFile != "" {
if auth.SMTPCredentials != nil {
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
srv.AuthHandler = authHandler
srv.AuthRequired = true

View File

@@ -13,7 +13,7 @@ export default {
beforeMount() {
document.title = document.title + ' - ' + location.hostname
mailbox.showTagColors = localStorage.getItem('showTagsColors') == '1'
mailbox.showTagColors = !localStorage.getItem('hideTagColors') == '1'
// load global config
this.get(this.resolve('/api/v1/webui'), false, function (response) {

View File

@@ -94,11 +94,29 @@
}
.message {
.subject {
color: $text-muted;
b {
color: $list-group-color;
}
small {
opacity: 0.5;
}
}
&.read {
color: $text-muted;
b {
opacity: 0.7;
font-weight: normal;
color: $list-group-color;
}
small {
opacity: 0.5;
}
}
&.selected {

View File

@@ -159,8 +159,12 @@ export default {
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning mb-3" v-if="mailbox.appInfo.LatestVersion == ''">
There might be a newer version available. The check failed.
</div>
<a class="btn btn-warning d-block mb-3"
v-if="mailbox.appInfo.Version != mailbox.appInfo.LatestVersion"
v-else-if="mailbox.appInfo.Version != mailbox.appInfo.LatestVersion"
:href="'https://github.com/axllent/mailpit/releases/tag/' + mailbox.appInfo.LatestVersion">
A new version of Mailpit ({{ mailbox.appInfo.LatestVersion }}) is available.
</a>
@@ -179,8 +183,7 @@ export default {
</a>
</div>
<div class="col-sm-6">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki"
target="_blank">
<a class="btn btn-primary w-100" href="https://mailpit.axllent.org/docs/" target="_blank">
Documentation
</a>
</div>

View File

@@ -8,6 +8,10 @@ export default {
CommonMixins
],
props: {
loadingMessages: Number, // use different name to `loading` as that is already in use in CommonMixins
},
data() {
return {
mailbox,
@@ -137,8 +141,13 @@ export default {
</div>
</div>
<div class="col-lg-6 col-xxl-7 mt-2 mt-lg-0">
<div><b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b></div>
<div>
<div class="subject text-truncate">
<b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b>
</div>
<div v-if="message.Snippet != ''" class="small text-muted text-truncate">
{{ message.Snippet }}
</div>
<div v-if="message.Tags.length">
<RouterLink class="badge me-1" v-for="t in message.Tags" :to="'/search?q=' + tagEncodeURI(t)"
:style="mailbox.showTagColors ? { backgroundColor: colorHash(t) } : { backgroundColor: '#6c757d' }"
:title="'Filter messages tagged with ' + t">
@@ -153,13 +162,15 @@ export default {
<div class="d-none d-lg-block col-2 col-xxl-1 small text-end text-muted">
{{ getRelativeCreated(message) }}
</div>
<!-- </a> -->
</RouterLink>
</div>
</template>
<template v-else>
<p class="text-center mt-5">
<template v-if="getSearch()">No results for <code>{{ getSearch() }}</code></template>
<span v-if="loadingMessages > 0" class="text-secondary">
Loading messages...
</span>
<template v-else-if="getSearch()">No results for <code>{{ getSearch() }}</code></template>
<template v-else>No messages in your mailbox</template>
</p>
</template>

View File

@@ -81,7 +81,7 @@ export default {
</button>
<button class="list-group-item list-group-item-action" data-bs-toggle="modal"
data-bs-target="#DeleteAllModal" :disabled="!mailbox.count">
data-bs-target="#DeleteAllModal" :disabled="!mailbox.total">
<i class="bi bi-trash-fill me-1 text-danger"></i>
Delete all
</button>

View File

@@ -19,7 +19,7 @@ export default {
return false
}
let re = new RegExp(`\\btag:"?${tag}"?\\b`, 'i')
let re = new RegExp(`(^|\\s)tag:"?${tag}"?($|\\s)`, 'i')
return query.match(re)
}
}

View File

@@ -293,7 +293,8 @@ export default {
</span>
</td>
</tr>
<tr v-if="message.ReturnPath && message.ReturnPath != message.From.Address" class="small">
<tr v-if="message.ReturnPath && message.From && message.ReturnPath != message.From.Address"
class="small">
<th class="text-nowrap">Return-Path</th>
<td class="privacy text-body-secondary text-break">
&lt;<a :href="searchURI(message.ReturnPath)" class="text-body-secondary">
@@ -443,7 +444,7 @@ export default {
aria-labelledby="nav-html-tab" tabindex="0">
<div id="responsive-view" :class="scaleHTMLPreview" :style="responsiveSizes[scaleHTMLPreview]">
<iframe target-blank="" class="tab-pane d-block" id="preview-html" :srcdoc="sanitizeHTML(message.HTML)"
v-on:load="resizeIframe" frameborder="0" style="width: 100%; height: 100%;">
v-on:load="resizeIframe" frameborder="0" style="width: 100%; height: 100%; background: #fff;">
</iframe>
</div>
<Attachments v-if="allAttachments(message).length" :message="message"

View File

@@ -14,9 +14,12 @@ export default {
AjaxLoader,
},
emits: ['delete'],
data() {
return {
addresses: [],
deleteAfterRelease: false,
mailbox,
allAddresses: [],
}
@@ -62,6 +65,9 @@ export default {
self.post(self.resolve('/api/v1/message/' + self.message.ID + '/release'), data, function (response) {
self.modal("ReleaseModal").hide()
if (self.deleteAfterRelease) {
self.$emit('delete')
}
})
}, 100)
}
@@ -83,7 +89,7 @@ export default {
<label class="col-sm-2 col-form-label text-body-secondary">From</label>
<div class="col-sm-10">
<input type="text" aria-label="From address" readonly class="form-control-plaintext"
:value="message.From.Address">
:value="message.From ? message.From.Address : ''">
</div>
</div>
<div class="row">
@@ -108,6 +114,18 @@ export default {
<div class="invalid-feedback">Invalid email address</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-10 offset-sm-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="deleteAfterRelease"
id="DeleteAfterRelease">
<label class="form-check-label" for="DeleteAfterRelease">
Delete the message after release
</label>
</div>
</div>
</div>
<div class="form-text text-center" v-if="mailbox.uiConfig.MessageRelay.RecipientAllowlist != ''">
Note: A recipient allowlist has been configured. Any mail address not matching it will be rejected.
<br class="d-none d-md-inline">

View File

@@ -7,7 +7,7 @@ Tinycon.setOptions({
height: 11,
background: '#dd0000',
fallback: false,
font: '9px arial',
font: '9px arial, sans-serif',
})
// global mailbox info
@@ -17,7 +17,7 @@ export const mailbox = reactive({
count: 0, // total in mailbox or search
messages: [], // current messages
tags: [], // all tags
showTagColors: false, // show tag colors?
showTagColors: true, // show/hide tag colors
selected: [], // currently selected
connected: false, // websocket connection
searching: false, // current search, false for none
@@ -51,9 +51,9 @@ watch(
() => mailbox.showTagColors,
(v) => {
if (v) {
localStorage.setItem('showTagsColors', '1')
localStorage.removeItem('hideTagColors')
} else {
localStorage.removeItem('showTagsColors')
localStorage.setItem('hideTagColors', '1')
}
}
)

View File

@@ -83,7 +83,7 @@ export default {
<div class="col-xl-10 col-md-9 mh-100 ps-0 ps-md-2 pe-0">
<div class="mh-100" style="overflow-y: auto;" id="message-page">
<ListMessages />
<ListMessages :loading-messages="loading" />
</div>
</div>
</div>

View File

@@ -341,6 +341,6 @@ export default {
<AboutMailpit modals />
<AjaxLoader :loading="loading" />
<Release v-if="message" ref="ReleaseRef" :message="message" />
<Release v-if="mailbox.uiConfig.MessageRelay && message" ref="ReleaseRef" :message="message" @delete="deleteMessage" />
<Screenshot v-if="message" ref="ScreenshotRef" :message="message" />
</template>

View File

@@ -102,16 +102,16 @@ export default {
</div>
<div class="row flex-fill" style="min-height:0">
<div class="d-none d-md-block col-lg-2 col-md-3 mh-100 position-relative"
<div class="d-none d-md-block col-xl-2 col-md-3 mh-100 position-relative"
style="overflow-y: auto; overflow-x: hidden;">
<NavSearch @loadMessages="loadMessages" />
<NavTags />
<AboutMailpit />
</div>
<div class="col-lg-10 col-md-9 mh-100 ps-0 ps-md-2 pe-0">
<div class="col-xl-10 col-md-9 mh-100 ps-0 ps-md-2 pe-0">
<div class="mh-100" style="overflow-y: auto;" id="message-page">
<ListMessages />
<ListMessages :loading-messages="loading" />
</div>
</div>
</div>

View File

@@ -66,7 +66,7 @@
"parameters": [
{
"type": "string",
"description": "Database ID",
"description": "Message database ID",
"name": "ID",
"in": "path",
"required": true
@@ -136,11 +136,11 @@
"Other"
],
"summary": "HTML check (beta)",
"operationId": "HTMLCheckResponse",
"operationId": "HTMLCheck",
"parameters": [
{
"type": "string",
"description": "Database ID",
"description": "Message database ID",
"name": "ID",
"in": "path",
"required": true
@@ -173,18 +173,19 @@
"Other"
],
"summary": "Link check (beta)",
"operationId": "LinkCheckResponse",
"operationId": "LinkCheck",
"parameters": [
{
"type": "string",
"description": "Database ID",
"description": "Message database ID",
"name": "ID",
"in": "path",
"required": true
},
{
"type": "boolean",
"default": false,
"type": "string",
"default": "false",
"x-go-name": "Follow",
"description": "Follow redirects",
"name": "follow",
"in": "query"
@@ -223,7 +224,7 @@
"parameters": [
{
"type": "string",
"description": "Database ID",
"description": "Message database ID",
"name": "ID",
"in": "path",
"required": true
@@ -323,7 +324,7 @@
},
"/api/v1/message/{ID}/release": {
"post": {
"description": "Release a message via a pre-configured external SMTP server..",
"description": "Release a message via a pre-configured external SMTP server. This is only enabled if message relaying has been configured.",
"consumes": [
"application/json"
],
@@ -338,24 +339,20 @@
"message"
],
"summary": "Release message",
"operationId": "Release",
"operationId": "ReleaseMessage",
"parameters": [
{
"type": "string",
"description": "Database ID",
"description": "Message database ID",
"name": "ID",
"in": "path",
"required": true
},
{
"description": "Array of email addresses to release message to",
"name": "to",
"name": "Body",
"in": "body",
"required": true,
"schema": {
"description": "Array of email addresses to release message to",
"type": "object",
"$ref": "#/definitions/ReleaseMessageRequest"
"$ref": "#/definitions/releaseMessageRequestBody"
}
}
],
@@ -428,13 +425,10 @@
"operationId": "SetReadStatus",
"parameters": [
{
"description": "Database IDs to update",
"name": "ids",
"name": "Body",
"in": "body",
"schema": {
"description": "Database IDs to update",
"type": "object",
"$ref": "#/definitions/SetReadStatusRequest"
"$ref": "#/definitions/setReadStatusRequestBody"
}
}
],
@@ -448,7 +442,7 @@
}
},
"delete": {
"description": "If no IDs are provided then all messages are deleted.",
"description": "Delete individual or all messages. If no IDs are provided then all messages are deleted.",
"consumes": [
"application/json"
],
@@ -463,15 +457,12 @@
"messages"
],
"summary": "Delete messages",
"operationId": "Delete",
"operationId": "DeleteMessages",
"parameters": [
{
"description": "Database IDs to delete",
"name": "ids",
"name": "Body",
"in": "body",
"schema": {
"description": "Database IDs to delete",
"type": "object",
"$ref": "#/definitions/DeleteRequest"
}
}
@@ -534,7 +525,7 @@
}
},
"delete": {
"description": "Deletes messages matching a search.",
"description": "Delete all messages matching a search.",
"produces": [
"application/json"
],
@@ -546,7 +537,7 @@
"messages"
],
"summary": "Delete messages by search",
"operationId": "MessagesSummary",
"operationId": "DeleteSearch",
"parameters": [
{
"type": "string",
@@ -580,7 +571,7 @@
"tags"
],
"summary": "Get all current tags",
"operationId": "SetTags",
"operationId": "GetTags",
"responses": {
"200": {
"$ref": "#/responses/ArrayResponse"
@@ -591,7 +582,7 @@
}
},
"put": {
"description": "To remove all tags from a message, pass an empty tags array.",
"description": "This will overwrite any existing tags for selected message database IDs. To remove all tags from a message, pass an empty tags array.",
"consumes": [
"application/json"
],
@@ -609,14 +600,10 @@
"operationId": "SetTags",
"parameters": [
{
"description": "Database IDs to update",
"name": "ids",
"name": "Body",
"in": "body",
"required": true,
"schema": {
"description": "Database IDs to update",
"type": "object",
"$ref": "#/definitions/SetTagsRequest"
"$ref": "#/definitions/setTagsRequestBody"
}
}
],
@@ -654,6 +641,74 @@
}
}
}
},
"/view/{ID}.html": {
"get": {
"description": "Renders just the message's HTML part which can be used for UI integration testing.\nAttached inline images are modified to link to the API provided they exist.\nNote that is the message does not contain a HTML part then an 404 error is returned.\n\nThe ID can be set to `latest` to return the latest message.",
"produces": [
"text/html"
],
"schemes": [
"http",
"https"
],
"tags": [
"testing"
],
"summary": "Render message HTML part",
"operationId": "GetMessageHTML",
"parameters": [
{
"type": "string",
"description": "Database ID or latest",
"name": "ID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/HTMLResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/view/{ID}.txt": {
"get": {
"description": "Renders just the message's text part which can be used for UI integration testing.\n\nThe ID can be set to `latest` to return the latest message.",
"produces": [
"text/plain"
],
"schemes": [
"http",
"https"
],
"tags": [
"testing"
],
"summary": "Render message text part",
"operationId": "GetMessageText",
"parameters": [
{
"type": "string",
"description": "Database ID or latest",
"name": "ID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/TextResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
}
},
"definitions": {
@@ -732,22 +787,26 @@
"format": "int64"
}
},
"x-go-package": "github.com/axllent/mailpit/storage"
"x-go-package": "github.com/axllent/mailpit/internal/storage"
},
"DeleteRequest": {
"description": "Delete request",
"type": "object",
"properties": {
"ids": {
"description": "ids\nin:body",
"description": "Array of message database IDs",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "IDs"
"x-go-name": "IDs",
"example": [
"5dec4247-812e-4b77-9101-e25ad406e9ea",
"8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"
]
}
},
"x-go-name": "deleteRequest",
"x-go-name": "deleteMessagesRequestBody",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"HTMLCheckResponse": {
@@ -776,7 +835,7 @@
}
},
"x-go-name": "Response",
"x-go-package": "github.com/axllent/mailpit/utils/htmlcheck"
"x-go-package": "github.com/axllent/mailpit/internal/htmlcheck"
},
"HTMLCheckResult": {
"description": "Result struct",
@@ -808,7 +867,7 @@
}
},
"x-go-name": "Result",
"x-go-package": "github.com/axllent/mailpit/utils/htmlcheck"
"x-go-package": "github.com/axllent/mailpit/internal/htmlcheck"
},
"HTMLCheckScore": {
"description": "Score struct",
@@ -836,7 +895,7 @@
}
},
"x-go-name": "Score",
"x-go-package": "github.com/axllent/mailpit/utils/htmlcheck"
"x-go-package": "github.com/axllent/mailpit/internal/htmlcheck"
},
"HTMLCheckTotal": {
"description": "Total weighted result for all scores",
@@ -869,7 +928,7 @@
}
},
"x-go-name": "Total",
"x-go-package": "github.com/axllent/mailpit/utils/htmlcheck"
"x-go-package": "github.com/axllent/mailpit/internal/htmlcheck"
},
"HTMLCheckWarning": {
"description": "Warning represents a failed test",
@@ -925,7 +984,7 @@
}
},
"x-go-name": "Warning",
"x-go-package": "github.com/axllent/mailpit/utils/htmlcheck"
"x-go-package": "github.com/axllent/mailpit/internal/htmlcheck"
},
"Link": {
"description": "Link struct",
@@ -945,7 +1004,7 @@
"type": "string"
}
},
"x-go-package": "github.com/axllent/mailpit/utils/linkcheck"
"x-go-package": "github.com/axllent/mailpit/internal/linkcheck"
},
"LinkCheckResponse": {
"description": "Response represents the Link check response",
@@ -965,7 +1024,7 @@
}
},
"x-go-name": "Response",
"x-go-package": "github.com/axllent/mailpit/utils/linkcheck"
"x-go-package": "github.com/axllent/mailpit/internal/linkcheck"
},
"Message": {
"description": "Message data excluding physical attachments",
@@ -1058,7 +1117,7 @@
}
}
},
"x-go-package": "github.com/axllent/mailpit/storage"
"x-go-package": "github.com/axllent/mailpit/internal/storage"
},
"MessageHeaders": {
"description": "Message headers",
@@ -1120,6 +1179,10 @@
"type": "integer",
"format": "int64"
},
"Snippet": {
"description": "Message snippet includes up to 250 characters",
"type": "string"
},
"Subject": {
"description": "Email subject",
"type": "string"
@@ -1139,14 +1202,14 @@
}
}
},
"x-go-package": "github.com/axllent/mailpit/storage"
"x-go-package": "github.com/axllent/mailpit/internal/storage"
},
"MessagesSummary": {
"description": "MessagesSummary is a summary of a list of messages",
"type": "object",
"properties": {
"messages": {
"description": "Messages summary\nin:body",
"description": "Messages summary\nin: body",
"type": "array",
"items": {
"$ref": "#/definitions/MessageSummary"
@@ -1188,67 +1251,6 @@
},
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"ReleaseMessageRequest": {
"description": "Release request",
"type": "object",
"properties": {
"to": {
"description": "To\nin:body",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "To"
}
},
"x-go-name": "releaseMessageRequest",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"SetReadStatusRequest": {
"description": "Set read status request",
"type": "object",
"properties": {
"ids": {
"description": "ids\nin:body",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "IDs"
},
"read": {
"description": "Read status",
"type": "boolean",
"x-go-name": "Read"
}
},
"x-go-name": "setReadStatusRequest",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"SetTagsRequest": {
"description": "Set tags request",
"type": "object",
"properties": {
"ids": {
"description": "IDs\nin:body",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "IDs"
},
"tags": {
"description": "Tags\nin:body",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "Tags"
}
},
"x-go-name": "setTagsRequest",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"WebUIConfiguration": {
"description": "Response includes global web UI settings",
"type": "object",
@@ -1282,6 +1284,89 @@
},
"x-go-name": "webUIConfiguration",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"releaseMessageRequestBody": {
"description": "Release request",
"type": "object",
"required": [
"to"
],
"properties": {
"to": {
"description": "Array of email addresses to relay the message to",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "To",
"example": [
"user1@example.com",
"user2@example.com"
]
}
},
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"setReadStatusRequestBody": {
"description": "Set read status request",
"type": "object",
"properties": {
"ids": {
"description": "Array of message database IDs",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "IDs",
"example": [
"5dec4247-812e-4b77-9101-e25ad406e9ea",
"8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"
]
},
"read": {
"description": "Read status",
"type": "boolean",
"default": false,
"x-go-name": "Read",
"example": true
}
},
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"setTagsRequestBody": {
"description": "Set tags request",
"type": "object",
"required": [
"tags",
"ids"
],
"properties": {
"ids": {
"description": "Array of message database IDs",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "IDs",
"example": [
"5dec4247-812e-4b77-9101-e25ad406e9ea",
"8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"
]
},
"tags": {
"description": "Array of tag names to set",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "Tags",
"example": [
"Tag 1",
"Tag 2"
]
}
},
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
}
},
"responses": {
@@ -1295,20 +1380,27 @@
}
},
"BinaryResponse": {
"description": "Binary data response inherits the attachment's content type"
"description": "Binary data response inherits the attachment's content type",
"schema": {
"type": "string"
}
},
"ErrorResponse": {
"description": "Error response"
"description": "HTTP error response will return with a \u003e= 400 response code",
"schema": {
"type": "string"
}
},
"HTMLResponse": {
"description": "HTML response",
"schema": {
"type": "string"
}
},
"InfoResponse": {
"description": "Application information",
"schema": {
"$ref": "#/definitions/AppInformation"
},
"headers": {
"Body": {
"description": "Application information"
}
}
},
"MessagesSummaryResponse": {
@@ -1318,20 +1410,21 @@
}
},
"OKResponse": {
"description": "Plain text \"ok\" response"
"description": "Plain text \"ok\" response",
"schema": {
"type": "string"
}
},
"TextResponse": {
"description": "Plain text response"
"description": "Plain text response",
"schema": {
"type": "string"
}
},
"WebUIConfigurationResponse": {
"description": "Web UI configuration",
"schema": {
"$ref": "#/definitions/WebUIConfiguration"
},
"headers": {
"Body": {
"description": "Web UI configuration settings"
}
}
}
}

72
server/webhook/webhook.go Normal file
View File

@@ -0,0 +1,72 @@
// Package webhook will optionally call a preconfigured endpoint
package webhook
import (
"bytes"
"encoding/json"
"net/http"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"golang.org/x/time/rate"
)
var (
// RateLimit is the minimum number of seconds between requests
RateLimit = 1
rl rate.Sometimes
rateLimiterSet bool
)
// Send will post the MessageSummary to a webhook (if configured)
func Send(msg interface{}) {
if config.WebhookURL == "" {
return
}
if !rateLimiterSet {
if RateLimit > 0 {
rl = rate.Sometimes{Interval: time.Duration(RateLimit) * time.Second}
} else {
// run 1000 per second - ie: do not limit
rl = rate.Sometimes{First: 1000, Interval: time.Second}
}
rateLimiterSet = true
}
go func() {
rl.Do(func() {
b, err := json.Marshal(msg)
if err != nil {
logger.Log().Errorf("[webhook] invalid data: %s", err)
return
}
req, err := http.NewRequest("POST", config.WebhookURL, bytes.NewBuffer(b))
if err != nil {
logger.Log().Errorf("[webhook] error: %s", err)
return
}
req.Header.Set("User-Agent", "Mailpit/"+config.Version)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Log().Errorf("[webhook] error sending data: %s", err)
return
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
logger.Log().Warningf("[webhook] %s returned a %d status", config.WebhookURL, resp.StatusCode)
return
}
defer resp.Body.Close()
})
}()
}

View File

@@ -8,8 +8,8 @@ import (
"net/http"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/gorilla/websocket"
)
@@ -53,7 +53,24 @@ type Client struct {
send chan []byte
}
// writePump pumps messages from the hub to the websocket connection.
// ReadPump is used here solely to monitor the connection, not to actually receive messages.
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
}()
for {
_, _, err := c.conn.NextReader()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
logger.Log().Errorf("[websocket] error: %v", err)
}
break
}
}
}
// WritePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
@@ -62,7 +79,7 @@ func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
_ = c.conn.Close()
c.hub.unregister <- c
}()
for {
select {
@@ -99,19 +116,17 @@ func (c *Client) writePump() {
// ServeWs handles websocket requests from the peer.
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
if config.UIAuthFile != "" {
if config.UIAuthFile != "" {
user, pass, ok := r.BasicAuth()
if auth.UICredentials != nil {
user, pass, ok := r.BasicAuth()
if !ok {
basicAuthResponse(w)
return
}
if !ok {
basicAuthResponse(w)
return
}
if !config.UIAuth.Match(user, pass) {
basicAuthResponse(w)
return
}
if !auth.UICredentials.Match(user, pass) {
basicAuthResponse(w)
return
}
}
@@ -124,8 +139,8 @@ func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
// Allow collection of memory referenced by the caller by doing all work in
// new goroutines.
// Allow collection of memory referenced by the caller by doing all work in new goroutines.
go client.readPump()
go client.writePump()
}

View File

@@ -1,13 +1,10 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package websockets is used to broadcast messages to connected clients
package websockets
import (
"encoding/json"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/mailpit/internal/logger"
)
// Hub maintains the set of active clients and broadcasts messages to the
@@ -47,14 +44,17 @@ func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.Clients[client] = true
if _, ok := h.Clients[client]; !ok {
logger.Log().Debugf("[websocket] client %s connected", client.conn.RemoteAddr().String())
h.Clients[client] = true
}
case client := <-h.unregister:
if _, ok := h.Clients[client]; ok {
logger.Log().Debugf("[websocket] client %s disconnected", client.conn.RemoteAddr().String())
delete(h.Clients, client)
close(client.send)
}
case message := <-h.Broadcast:
// logger.Log().Debugf("[broadcast] %s", message)
for client := range h.Clients {
select {
case client.send <- message:
@@ -69,7 +69,7 @@ func (h *Hub) Run() {
// Broadcast will spawn a broadcast message to all connected clients
func Broadcast(t string, msg interface{}) {
if MessageHub == nil {
if MessageHub == nil || len(MessageHub.Clients) == 0 {
return
}
@@ -80,6 +80,7 @@ func Broadcast(t string, msg interface{}) {
if err != nil {
logger.Log().Errorf("[websocket] broadcast received invalid data: %s", err)
return
}
go func() { MessageHub.Broadcast <- b }()

View File

@@ -1,59 +0,0 @@
// Package tools provides various methods for various things
package tools
import (
"bufio"
"bytes"
"net/mail"
"regexp"
"github.com/axllent/mailpit/utils/logger"
)
// RemoveMessageHeaders scans a message for headers, if found them removes them.
// It will only remove a single instance of any header, and is intended to remove
// Bcc & Message-Id.
func RemoveMessageHeaders(msg []byte, headers []string) ([]byte, error) {
reader := bytes.NewReader(msg)
m, err := mail.ReadMessage(reader)
if err != nil {
return nil, err
}
reBlank := regexp.MustCompile(`^\s+`)
for _, hdr := range headers {
// case-insensitive
reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta(hdr+":"))
// header := []byte(hdr + ":")
if m.Header.Get(hdr) != "" {
scanner := bufio.NewScanner(bytes.NewReader(msg))
found := false
hdr := []byte("")
for scanner.Scan() {
line := scanner.Bytes()
if !found && reHdr.Match(line) {
// add the first line starting with <header>:
hdr = append(hdr, line...)
hdr = append(hdr, []byte("\r\n")...)
found = true
} else if found && reBlank.Match(line) {
// add any following lines starting with a whitespace (tab or space)
hdr = append(hdr, line...)
hdr = append(hdr, []byte("\r\n")...)
} else if found {
// stop scanning, we have the full <header>
break
}
}
if len(hdr) > 0 {
logger.Log().Debugf("[release] removing %s header", hdr)
msg = bytes.Replace(msg, hdr, []byte(""), 1)
}
}
}
return msg, nil
}