Compare commits

...

132 Commits

Author SHA1 Message Date
Ralph Slooten
fb03fda9ea Merge branch 'release/v1.4.0' 2023-03-12 15:08:18 +13:00
Ralph Slooten
e2254a68ef Release v1.4.0 2023-03-12 15:08:17 +13:00
Ralph Slooten
755ff37cdc Merge branch 'feature/smtp-auth' into develop 2023-03-12 15:06:44 +13:00
Ralph Slooten
03f30b01bf Update README 2023-03-12 15:06:26 +13:00
Ralph Slooten
27d49417d7 API: Return received datetime when message does not contain a date header 2023-03-12 14:27:20 +13:00
Ralph Slooten
aeeb732681 Feature: Rename SSL to TLS, add deprecation warnings to flags & ENV variables referring to SSL 2023-03-12 11:31:15 +13:00
Ralph Slooten
73a92a3952 Feature: Options to support auth without STARTTLS, and accept any login
@see #56
2023-03-12 10:51:49 +13:00
Ralph Slooten
9cd81afe7c Feature: Option to use message dates as received dates (new messages only) 2023-03-10 16:42:14 +13:00
Ralph Slooten
41270b956e Merge tag 'v1.3.11' into develop
Release v1.3.11
2023-03-10 14:57:09 +13:00
Ralph Slooten
3d31ae7da4 Merge branch 'release/v1.3.11' 2023-03-10 14:57:08 +13:00
Ralph Slooten
dfad730b21 Release v1.3.11 2023-03-10 14:57:08 +13:00
Ralph Slooten
f0723fb64a Update git-chglog format 2023-03-10 14:52:35 +13:00
Ralph Slooten
b905ba4ec5 Feature: Expand custom webroot path to include a-z A-Z 0-9 _ . - and /
@see #64
2023-03-10 14:48:28 +13:00
Ralph Slooten
7675cd162f Docker: Expose default ports (1025/tcp 8025/tcp)
@see #63
2023-03-09 15:49:47 +13:00
Ralph Slooten
dff5a605b4 Create FUNDING.yml 2023-03-09 01:18:00 +13:00
Ralph Slooten
3f3b8a6d97 Merge tag 'v1.3.10' into develop
Release v1.3.10
2023-03-04 23:40:31 +13:00
Ralph Slooten
fc595c031d Merge branch 'release/v1.3.10' 2023-03-04 23:40:28 +13:00
Ralph Slooten
a897004dc1 Release v1.3.10 2023-03-04 23:40:27 +13:00
Ralph Slooten
6917477533 Libs: Update node modules 2023-03-04 23:38:05 +13:00
Ralph Slooten
eede2bff99 Bugfix: Fix search with existing emails 2023-03-04 23:25:55 +13:00
Ralph Slooten
de0549e60a Change Dependabot checks to monthly 2023-02-27 21:57:38 +13:00
Ralph Slooten
17caa21afd Update libs 2023-02-27 21:56:47 +13:00
Ralph Slooten
4656717046 Merge tag 'v1.3.9' into develop
Release v1.3.9
2023-02-24 22:31:25 +13:00
Ralph Slooten
72fdbb8364 Merge branch 'release/v1.3.9' 2023-02-24 22:31:21 +13:00
Ralph Slooten
37b4f1f566 Release v1.3.9 2023-02-24 22:31:21 +13:00
Ralph Slooten
464fbf818c Merge pull request #44 from axllent/dependabot/github_actions/wangyoucao577/go-release-action-1.36
Bump wangyoucao577/go-release-action from 1.30 to 1.36
2023-02-24 21:53:47 +13:00
Ralph Slooten
6360a69ff6 Merge pull request #43 from axllent/dependabot/github_actions/docker/build-push-action-4
Bump docker/build-push-action from 3 to 4
2023-02-24 21:53:19 +13:00
Ralph Slooten
054438b952 Libs: Update node modules
Note: due to an  incompatibility issue, esbuild-sass-plugin has been left as-is, see: https://github.com/glromeo/esbuild-sass-plugin/issues/121
2023-02-24 21:50:58 +13:00
Ralph Slooten
cb6085790b Libs: Update Go modules 2023-02-24 21:39:45 +13:00
Ralph Slooten
1bd0c6ac74 Feature: Add Cc and Bcc search filters 2023-02-24 21:36:42 +13:00
Ralph Slooten
7cb46ba869 Correctly case Cc and Bcc in UI 2023-02-24 21:36:42 +13:00
Ralph Slooten
6efe99ffdf Move smtpd to server 2023-02-24 21:36:42 +13:00
Ralph Slooten
cc121e4b27 Merge pull request #55 from axllent/dependabot/go_modules/golang.org/x/image-0.5.0
Bump golang.org/x/image from 0.3.0 to 0.5.0
2023-02-24 21:30:55 +13:00
dependabot[bot]
ee86260651 Bump golang.org/x/image from 0.3.0 to 0.5.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.3.0 to 0.5.0.
- [Release notes](https://github.com/golang/image/releases)
- [Commits](https://github.com/golang/image/compare/v0.3.0...v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-24 08:24:21 +00:00
dependabot[bot]
cab9f8a729 Bump wangyoucao577/go-release-action from 1.30 to 1.36
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.30 to 1.36.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.30...v1.36)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-24 08:23:12 +00:00
dependabot[bot]
790fbe69fd Bump docker/build-push-action from 3 to 4
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-24 08:23:11 +00:00
Ralph Slooten
51074a9d72 Merge pull request #42 from shizunge/dependabot
Add dependabot.yml
2023-02-24 21:22:51 +13:00
Shizun Ge
28b4f2d09d add npm to dependabot.yml 2023-02-23 20:23:37 -08:00
Shizun Ge
b6c1c180c9 add dependabot.yml 2023-02-22 23:31:30 -08:00
Ralph Slooten
264ad1bf9f Merge tag 'v1.3.8' into develop
Release v1.3.8
2023-02-09 15:24:41 +13:00
Ralph Slooten
7d63c75557 Merge branch 'release/v1.3.8' 2023-02-09 15:24:35 +13:00
Ralph Slooten
0c4c2881c8 Release v1.3.8 2023-02-09 15:24:35 +13:00
Ralph Slooten
56999e97e2 UI: Compress SVG icons 2023-02-09 15:21:46 +13:00
Ralph Slooten
d238675011 Bugfix: Restore notification icon
Fixes #34
2023-02-09 15:21:08 +13:00
Ralph Slooten
fea3b0a422 Update README 2023-02-06 15:27:08 +13:00
Ralph Slooten
24b1dfa040 Update REDAME 2023-02-06 15:24:36 +13:00
Ralph Slooten
ab73a4bcfb Merge tag 'v1.3.7' into develop
Release v1.3.7
2023-01-30 22:21:28 +13:00
Ralph Slooten
df3b27b5e0 Merge branch 'release/v1.3.7' 2023-01-30 22:21:22 +13:00
Ralph Slooten
52bf19a40c Release v1.3.7 2023-01-30 22:21:22 +13:00
Matthias Fechner
c1694f1a22 Feature: Add Kubernetes API health (livez/readyz) endpoints
Kubernetes checks if a pod is ok and if it can retrieve traffic using probes.
This commit add two routes to make a liveness probe and a readiness probe.
2023-01-30 22:17:54 +13:00
Ralph Slooten
894da47eda Libs: Upgrade to esbuild 0.17.5 2023-01-30 22:01:34 +13:00
Ralph Slooten
1718ec00e5 Fix typo 2023-01-14 00:31:03 +13:00
Ralph Slooten
70df34d071 Merge tag 'v1.3.6' into develop
Release v1.3.6
2023-01-12 16:12:20 +13:00
Ralph Slooten
d101ec045d Merge branch 'release/v1.3.6' 2023-01-12 16:12:18 +13:00
Ralph Slooten
a1d8840da2 Release v1.3.6 2023-01-12 16:12:18 +13:00
Ralph Slooten
ed1bb83bda Libs: Update node modules 2023-01-12 16:09:27 +13:00
Ralph Slooten
4b2e8b0174 Libs: Update go modules 2023-01-12 16:07:38 +13:00
Ralph Slooten
594c4817a4 Bugfix: Correctly index missing 'From' header in database
When an email with a missing `From: ` header is stored in the database, a null value was stored. This broke the search. Fixes #31
2023-01-12 16:04:08 +13:00
Ralph Slooten
47a556d05e Merge tag 'v1.3.5' into develop
Release v1.3.5
2023-01-05 11:58:29 +13:00
Ralph Slooten
e3e7c09e81 Merge branch 'release/v1.3.5' 2023-01-05 11:58:06 +13:00
Ralph Slooten
98a932ecdb Release v1.3.5 2023-01-05 11:57:53 +13:00
Ralph Slooten
d47eb09c54 Bugfix: Include HTML link text in search data
`<a href="https://example.com">search text</a>` now stores `search text https://example.com` in the database.

Resolves #30
2023-01-05 11:55:18 +13:00
Ralph Slooten
acee53537c Add automation to close stale issues 2022-12-23 16:20:41 +13:00
Ralph Slooten
b18bcebd51 Fix error casing 2022-12-15 22:09:03 +13:00
Ralph Slooten
0502056678 Merge tag 'v1.3.4' into develop
Release v1.3.4
2022-12-09 10:28:24 +13:00
Ralph Slooten
6901a20661 Merge branch 'release/v1.3.4' 2022-12-09 10:28:21 +13:00
Ralph Slooten
10752a58c8 Release v1.3.4 2022-12-09 10:28:21 +13:00
Ralph Slooten
c8bf742c18 Bugfix: Allow tags to be set from MP_TAG environment
Relates to #26
2022-12-09 10:27:37 +13:00
Ralph Slooten
7313862ad5 Merge tag 'v1.3.3' into develop
Release v1.3.3
2022-12-09 09:33:05 +13:00
Ralph Slooten
8976124b3d Merge branch 'release/v1.3.3' 2022-12-09 09:33:03 +13:00
Ralph Slooten
4fbff688ec Release v1.3.3 2022-12-09 09:33:03 +13:00
Ralph Slooten
dca70a50c3 Bugfix: Allow tags to be set from MP_TAG environment
Fixes #26
2022-12-09 09:32:07 +13:00
Ralph Slooten
eb50304a13 Merge tag 'v1.3.2' into develop
Release v1.3.2
2022-12-08 22:01:50 +13:00
Ralph Slooten
858dfca321 Merge branch 'release/v1.3.2' 2022-12-08 22:01:47 +13:00
Ralph Slooten
5e09dec667 Release v1.3.2 2022-12-08 22:01:47 +13:00
Ralph Slooten
638ea3efa8 ### Libs
- Upgrade esbuild to 0.16.2
2022-12-08 22:01:21 +13:00
Ralph Slooten
06bfc3b6e3 Build: Temporarily disable arm (32) Docker build
Seems to be causing github actions to get stuck for hours (11+).
2022-12-08 21:59:42 +13:00
Ralph Slooten
c2d34f3071 Merge tag 'v1.3.1' into develop
Release v1.3.1
2022-12-08 10:22:23 +13:00
Ralph Slooten
be582291c7 Merge branch 'release/v1.3.1' 2022-12-08 10:22:21 +13:00
Ralph Slooten
646fe072be Release v1.3.1 2022-12-08 10:22:20 +13:00
Ralph Slooten
deba47f6d1 Libs: Upgrade esbuild & axios 2022-12-08 10:15:24 +13:00
Ralph Slooten
5f9efebeb3 Bugfix: Append trailing slash to custom webroot for UI & API
Fixes #25
2022-12-08 09:54:03 +13:00
Ralph Slooten
06aa7a2dea Remove redundant offcanvas mixin 2022-12-03 17:21:51 +13:00
Ralph Slooten
2c3c436fc1 UI: Rename "results" to "result" when singular message returned 2022-11-25 19:06:41 +13:00
Ralph Slooten
6f2dd83936 Merge tag 'v1.3.0' into develop
Release v1.3.0
2022-11-22 22:25:06 +13:00
Ralph Slooten
b850c89ae0 Merge branch 'release/v1.3.0' 2022-11-22 22:24:55 +13:00
Ralph Slooten
cc327ab3ba Release v1.3.0 2022-11-22 22:24:55 +13:00
Ralph Slooten
1886d78001 Libs: Update go modules 2022-11-22 22:18:33 +13:00
Ralph Slooten
63cbafa182 Libs: Update node modules
Including axios, bootstrap, bootstrap5-tags, esbuild, esbuild-sass-plugin, vue
2022-11-22 22:16:46 +13:00
Ralph Slooten
95dacfc5db Build: Remove duplicate bootstrap CSS 2022-11-21 21:43:30 +13:00
Ralph Slooten
067d218f4b Merge tag 'v1.2.9' into develop
Release v1.2.9
2022-11-18 13:26:32 +13:00
Ralph Slooten
3dd004ea4b Merge branch 'release/v1.2.9' 2022-11-18 13:26:29 +13:00
Ralph Slooten
6570217bfd Release v1.2.9 2022-11-18 13:26:29 +13:00
Ralph Slooten
54635b748a Bugfix: Delay 200ms to set target="_blank" for all rendered email links
Fixes #22
2022-11-18 13:25:15 +13:00
Ralph Slooten
0ea4cab33b Merge tag 'v1.2.8' into develop
Release v1.2.8
2022-11-13 17:29:43 +13:00
Ralph Slooten
0fde942e0d Merge branch 'release/v1.2.8' 2022-11-13 17:29:41 +13:00
Ralph Slooten
b09d7ac75d Release v1.2.8 2022-11-13 17:29:40 +13:00
Ralph Slooten
fc2fdd20f6 Update README - add tagging 2022-11-13 17:26:29 +13:00
Ralph Slooten
cbbac40c0d Add MP_TAG environment option 2022-11-13 17:26:29 +13:00
Ralph Slooten
6bc02fd4d4 Feature: Message tags and auto-tagging
See #17
2022-11-13 17:26:29 +13:00
Ralph Slooten
57cfb2611c Use bytes.NewReader(data) instead of strings.NewReader(string(data)) 2022-11-13 17:26:28 +13:00
Ralph Slooten
ba24d145ff Bugfix: Return empty arrays rather than null for message To, CC, BCC, Inlines & Attachments
Bugfix: Return empty arrays rather than null for message To, CC, BCC, Inlines & Attachments
2022-11-13 17:26:17 +13:00
Ralph Slooten
376e799eb0 Update README 2022-11-13 17:26:17 +13:00
Ralph Slooten
1dfadda07e Use path.Join() instead of url.JoinPath() for < 1.19 compatibility 2022-11-13 17:26:17 +13:00
Ralph Slooten
fc0a7358ab Merge branch 'release/v1.2.7' 2022-10-31 22:15:25 +13:00
Ralph Slooten
d229b34d98 Release v1.2.7 2022-10-31 22:15:24 +13:00
Ralph Slooten
cbc3fe59a8 Feature: Allow custom webroot
Allow Mailpit to run on a custom webroot, resolves #19
2022-10-31 22:13:41 +13:00
Ralph Slooten
ab771cf76c Move utils to subfolder 2022-10-29 10:52:22 +13:00
Ralph Slooten
7a27e09d23 Merge tag 'v1.2.6' into develop
Release v1.2.6
2022-10-29 10:23:32 +13:00
Ralph Slooten
cdce989a9c Merge branch 'release/v1.2.6' 2022-10-29 10:23:30 +13:00
Ralph Slooten
61dd3eddc5 Release v1.2.6 2022-10-29 10:23:29 +13:00
Ralph Slooten
290e48d875 Libs: Update go modules 2022-10-29 10:22:12 +13:00
Ralph Slooten
e7ea94a5d2 Libs: Update node modules 2022-10-29 10:22:05 +13:00
Ralph Slooten
43bd2a18ea API: Provide structs of API v1 responses for use in client code
See #21
2022-10-21 22:55:15 +13:00
Ralph Slooten
ec95e58e13 Use ${{ github.ref_name }} for workflow build tags 2022-10-16 12:12:28 +13:00
Ralph Slooten
70ac9c73ea Release 1.2.5 2022-10-16 12:07:20 +13:00
Ralph Slooten
0fcdcdd5f6 Merge tag '1.2.5' into develop
Release 1.2.5
2022-10-16 12:04:51 +13:00
Ralph Slooten
ea12a1ee56 Merge branch 'release/1.2.5' 2022-10-16 12:04:30 +13:00
Ralph Slooten
9345ed60c6 Update screenshot 2022-10-16 12:01:40 +13:00
Ralph Slooten
0a13cf8304 Tidy JS code 2022-10-16 11:51:20 +13:00
Ralph Slooten
4ebbdab7c0 Snapshot memory usage first 2022-10-16 11:36:28 +13:00
Ralph Slooten
cea9518b4b UI mobile tweaks 2022-10-16 10:45:04 +13:00
Ralph Slooten
a9220277d6 Refresh first page after prune when !results 2022-10-16 10:21:57 +13:00
Ralph Slooten
bd45d9dffe UI: Broadcast "delete all" action to reload all connected clients 2022-10-16 08:37:46 +13:00
Ralph Slooten
baaf3a3a23 UI tweaks 2022-10-16 00:03:16 +13:00
Ralph Slooten
2e95a75d32 Update Vue 2022-10-15 23:46:53 +13:00
Ralph Slooten
53d2296ff5 Minor UI changes 2022-10-15 23:37:22 +13:00
Ralph Slooten
e8bf803ca0 UI: Load first page if paginated list returns 0 results 2022-10-15 23:30:09 +13:00
Ralph Slooten
d9dc000e89 UI: Theme changes 2022-10-15 23:14:51 +13:00
Ralph Slooten
205611856b UI: Bump build action to use node 18 2022-10-15 09:41:33 +13:00
Ralph Slooten
5d396b9f25 Update build workflow 2022-10-15 09:31:29 +13:00
Ralph Slooten
4b95c6bda0 Merge tag '1.2.4' into develop
Release 1.2.4
2022-10-15 09:02:19 +13:00
59 changed files with 2426 additions and 1347 deletions

View File

@@ -19,7 +19,7 @@ Notable changes to Mailpit will be documented in this file.
{{ range .Versions }}
{{- if .CommitGroups -}}
## {{ .Tag.Name }}
## [{{ .Tag.Name }}]
{{ if .NoteGroups -}}
{{ range .NoteGroups -}}

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [axllent]

23
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
- package-ecosystem: "docker"
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -10,10 +10,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
@@ -27,11 +23,12 @@ jobs:
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/386,linux/amd64,linux/arm,linux/arm64
# platforms: linux/386,linux/amd64,linux/arm,linux/arm64
platforms: linux/386,linux/amd64,linux/arm64
build-args: |
"VERSION=${{ steps.tag.outputs.tag }}"
"VERSION=${{ github.ref_name }}"
push: true
tags: axllent/mailpit:latest,axllent/mailpit:${{ steps.tag.outputs.tag }}
tags: axllent/mailpit:latest,axllent/mailpit:${{ github.ref_name }}

View File

@@ -0,0 +1,23 @@
name: Close stale issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v7.0.0
with:
days-before-issue-stale: 30
days-before-issue-close: 7
exempt-issue-labels: "enhancement,bug"
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 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
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -23,20 +23,17 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
# build the assets
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: 'npm'
- run: echo "Building assets for ${{ github.ref_name }}"
- run: npm install
- run: npm run package
# build the binaries
- uses: wangyoucao577/go-release-action@v1.30
- uses: wangyoucao577/go-release-action@v1.36
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
@@ -46,4 +43,5 @@ jobs:
asset_name: mailpit-${{ matrix.goos }}-${{ matrix.goarch }}
extra_files: LICENSE README.md
md5sum: false
ldflags: -w -X "github.com/axllent/mailpit/config.Version=${{ steps.tag.outputs.tag }}"
overwrite: true
ldflags: -w -X "github.com/axllent/mailpit/config.Version=${{ github.ref_name }}"

View File

@@ -30,7 +30,7 @@ jobs:
# build the assets
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: 'npm'
- run: npm install
- run: npm run package

View File

@@ -2,13 +2,172 @@
Notable changes to Mailpit will be documented in this file.
## 1.2.4
## [v1.4.0]
### API
- Return received datetime when message does not contain a date header
### Feature
- Rename SSL to TLS, add deprecation warnings to flags & ENV variables referring to SSL
- Options to support auth without STARTTLS, and accept any login
- Option to use message dates as received dates (new messages only)
## [v1.3.11]
### Docker
- Expose default ports (1025/tcp 8025/tcp)
### Feature
- Expand custom webroot path to include a-z A-Z 0-9 _ . - and /
## [v1.3.10]
### Bugfix
- Fix search with existing emails
### Libs
- Update node modules
## [v1.3.9]
### Feature
- Add Cc and Bcc search filters
### Libs
- Update node modules
- Update Go modules
### Pull Requests
- Merge pull request [#44](https://github.com/axllent/mailpit/issues/44) from axllent/dependabot/github_actions/wangyoucao577/go-release-action-1.36
- Merge pull request [#43](https://github.com/axllent/mailpit/issues/43) from axllent/dependabot/github_actions/docker/build-push-action-4
- Merge pull request [#55](https://github.com/axllent/mailpit/issues/55) from axllent/dependabot/go_modules/golang.org/x/image-0.5.0
- Merge pull request [#42](https://github.com/axllent/mailpit/issues/42) from shizunge/dependabot
## [v1.3.8]
### Bugfix
- Restore notification icon
### UI
- Compress SVG icons
## [v1.3.7]
### Feature
- Add Kubernetes API health (livez/readyz) endpoints
### Libs
- Upgrade to esbuild 0.17.5
## [v1.3.6]
### Bugfix
- Correctly index missing 'From' header in database
### Libs
- Update node modules
- Update go modules
## [v1.3.5]
### Bugfix
- Include HTML link text in search data
## [v1.3.4]
### Bugfix
- Allow tags to be set from MP_TAG environment
## [v1.3.3]
### Bugfix
- Allow tags to be set from MP_TAG environment
## [v1.3.2]
### Build
- Temporarily disable arm (32) Docker build
## [v1.3.1]
### Bugfix
- Append trailing slash to custom webroot for UI & API
### Libs
- Upgrade esbuild & axios
### UI
- Rename "results" to "result" when singular message returned
## [v1.3.0]
### Build
- Remove duplicate bootstrap CSS
### Libs
- Update go modules
- Update node modules
## [v1.2.9]
### Bugfix
- Delay 200ms to set `target="_blank"` for all rendered email links
## [v1.2.8]
### Bugfix
- Return empty arrays rather than null for message To, CC, BCC, Inlines & Attachments
### Feature
- Message tags and auto-tagging
## [v1.2.7]
### Feature
- Allow custom webroot
## [v1.2.6]
### API
- Provide structs of API v1 responses for use in client code
### Libs
- Update go modules
- Update node modules
## [1.2.5]
### UI
- Broadcast "delete all" action to reload all connected clients
- Load first page if paginated list returns 0 results
- Theme changes
- Bump build action to use node 18
## [1.2.4]
### Bugfix
- Fix mail download link
## 1.2.3
## [1.2.3]
### API
- Add limit and start parameters to search
@@ -17,7 +176,7 @@ Notable changes to Mailpit will be documented in this file.
- Prevent double message index request on websocket connect
## 1.2.2
## [1.2.2]
### API
- Add API endpoint to return message headers
@@ -29,14 +188,14 @@ Notable changes to Mailpit will be documented in this file.
- Add API test for raw & message headers
## 1.2.1
## [1.2.1]
### UI
- Update frontend modules
- Add about app modal with version update notification
## 1.2.0
## [1.2.0]
### Feature
- Add REST API
@@ -49,13 +208,13 @@ Notable changes to Mailpit will be documented in this file.
- Hide delete all / mark all read in message view
## 1.1.7
## [1.1.7]
### Fix
- Normalize running binary name detection (Windows)
## 1.1.6
## [1.1.6]
### Fix
- Workaround for Safari source matching bug blocking event listener
@@ -64,7 +223,7 @@ Notable changes to Mailpit will be documented in this file.
- Add documentation link (wiki)
## 1.1.5
## [1.1.5]
### Build
- Switch to esbuild-sass-plugin
@@ -73,7 +232,7 @@ Notable changes to Mailpit will be documented in this file.
- Support for inline images using filenames instead of cid
## 1.1.4
## [1.1.4]
### Feature
- Add --quiet flag to display only errors
@@ -87,32 +246,32 @@ Notable changes to Mailpit will be documented in this file.
- Remove left & right borders (message list)
## 1.1.3
## [1.1.3]
### Fix
- Update message download link
## 1.1.2
## [1.1.2]
### UI
- Allow reverse proxy subdirectories
## 1.1.1
## [1.1.1]
### UI
- Attachment icons and image thumbnails
## 1.1.0
## [1.1.0]
### UI
- HTML source & highlighting
- Add previous/next message links
## 1.0.0
## [1.0.0]
### Feature
- Multiple message selection for group actions using shift/ctrl click
@@ -128,7 +287,7 @@ Notable changes to Mailpit will be documented in this file.
- Update frontend modules & esbuild
## 1.0.0-beta1
## [1.0.0-beta1]
### BREAKING CHANGE
@@ -141,7 +300,7 @@ This release includes a major backend storage change (SQLite) that will render a
- Resize preview iframe on load
## 0.1.5
## [0.1.5]
### Feature
- Improved message search - any order & phrase quoting
@@ -151,7 +310,7 @@ This release includes a major backend storage change (SQLite) that will render a
- Resize iframes with viewport resize
## 0.1.4
## [0.1.4]
### Feature
- Email compression in storage
@@ -164,7 +323,7 @@ This release includes a major backend storage change (SQLite) that will render a
- Mobile compatibility improvements & functionality
## 0.1.3
## [0.1.3]
### Feature
- Mark all messages as read
@@ -179,7 +338,7 @@ This release includes a major backend storage change (SQLite) that will render a
- Merge pull request [#6](https://github.com/axllent/mailpit/issues/6) from KaptinLin/develop
## 0.1.2
## [0.1.2]
### Feature
- Optional browser notifications (HTTPS only)
@@ -190,19 +349,19 @@ This release includes a major backend storage change (SQLite) that will render a
- Use strconv.Atoi() for safe string to int conversions
## 0.1.1
## [0.1.1]
### Bugfix
- Fix env variable for MP_UI_SSL_KEY
## 0.1.0
## [0.1.0]
### Feature
- SMTP STARTTLS & SMTP authentication support
## 0.0.9
## [0.0.9]
### Bugfix
- Include read status in search results
@@ -214,7 +373,7 @@ This release includes a major backend storage change (SQLite) that will render a
- Memory & physical database tests
## 0.0.8
## [0.0.8]
### Bugfix
- Fix total/unread count after failed message inserts
@@ -223,25 +382,25 @@ This release includes a major backend storage change (SQLite) that will render a
- Add project links to help in CLI
## 0.0.7
## [0.0.7]
### Bugfix
- Command flag should be `--auth-file`
## 0.0.6
## [0.0.6]
### Bugfix
- Disable CGO when building multi-arch binaries
## 0.0.5
## [0.0.5]
### Feature
- Basic authentication support
## 0.0.4
## [0.0.4]
### Bugfix
- Update to clover-v2.0.0-alpha.2 to fix sorting
@@ -258,13 +417,13 @@ This release includes a major backend storage change (SQLite) that will render a
- cater for messages without From email address
## 0.0.3
## [0.0.3]
### Bugfix
- Update to clover-v2.0.0-alpha.2 to fix sorting
## 0.0.2
## [0.0.2]
### Feature
- Unread statistics

View File

@@ -10,11 +10,12 @@ RUN apk add --no-cache git npm && \
npm install && npm run package && \
CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/axllent/mailpit/config.Version=${VERSION}" -o /mailpit
FROM alpine:latest
COPY --from=builder /mailpit /mailpit
RUN apk add --no-cache tzdata
EXPOSE 1025/tcp 8025/tcp
ENTRYPOINT ["/mailpit"]

View File

@@ -6,7 +6,7 @@
![CodeQL](https://github.com/axllent/mailpit/actions/workflows/codeql-analysis.yml/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/axllent/mailpit)](https://goreportcard.com/report/github.com/axllent/mailpit)
Mailpit is a multi-platform email testing tool for developers.
Mailpit is a multi-platform email testing tool 7 API for developers.
It acts as both an SMTP server, and provides a web interface to view all captured emails.
@@ -21,33 +21,50 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
- SMTP server (default `0.0.0.0:1025`)
- Web UI to view emails (formatted HTML, highlighted HTML source, text, raw source and MIME attachments including image thumbnails)
- 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 (HTTPS only)
- 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
- Can handle hundreds of thousands of emails
- Optional SMTP with STARTTLS & SMTP authentication ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication))
- Can handle tens of thousands of emails
- 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 allowing ([see docs](docs/apiv1/README.md))
- A simple REST API ([see docs](docs/apiv1/README.md))
- Multi-architecture [Docker images](https://github.com/axllent/mailpit/wiki/Docker-images)
## 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`.
Mailpit runs as a single binary and can be installed in different ways:
### Install via Brew (Mac)
Add the repository to your taps with `brew tap axllent/apps`, and then install Mailpit with `brew install mailpit`.
### Install via bash script (Linux & Mac)
Linux & Mac users can install it directly to `/usr/local/bin/mailpit` with:
```bash
sudo bash < <(curl -sL https://raw.githubusercontent.com/axllent/mailpit/develop/install.sh)
```
Or download a pre-built binary in the [releases](https://github.com/axllent/mailpit/releases/latest). The `mailpit` can be placed in your `$PATH`, or simply run as `./mailpit`. See `mailpit -h` for options, or see [the wiki](https://github.com/axllent/mailpit/wiki/Runtime-options) for additional information.
### 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`.
### Docker
See [Docker instructions](https://github.com/axllent/mailpit/wiki/Docker-images).
### Compile from source
To build Mailpit from source see [building from source](https://github.com/axllent/mailpit/wiki/Building-from-source).
The Mailpit web UI listens by default on `http://0.0.0.0:8025`, and the SMTP port on `0.0.0.0:1025`.
### Configuring sendmail

View File

@@ -1,15 +1,17 @@
// Package cmd is the main application
package cmd
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/server"
"github.com/axllent/mailpit/smtpd"
"github.com/axllent/mailpit/server/smtpd"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/spf13/cobra"
)
@@ -72,6 +74,66 @@ func init() {
rootCmd.PersistentFlags().BoolP("help", "h", false, "This help")
rootCmd.PersistentFlags().Lookup("help").Hidden = true
// load and warn deprecated ENV vars
initDeprecatedConfigFromEnv()
// load ENV vars
initConfigFromEnv()
rootCmd.Flags().StringVarP(&config.DataFile, "db-file", "d", config.DataFile, "Database file to store persistent data")
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
rootCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
rootCmd.Flags().BoolVar(&config.UseMessageDates, "use-message-dates", false, "Use message dates as the received dates")
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI 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")
rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication")
rootCmd.Flags().BoolVar(&config.SMTPAuthAcceptAny, "smtp-auth-accept-any", false, "Accept any SMTP username and password, including none")
rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-tls-cert", config.SMTPTLSCert, "TLS certificate for SMTP (STARTTLS) - requires smtp-tls-key")
rootCmd.Flags().StringVar(&config.SMTPTLSKey, "smtp-tls-key", config.SMTPTLSKey, "TLS key for SMTP (STARTTLS) - requires smtp-tls-cert")
rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", false, "Enable insecure PLAIN & LOGIN authentication")
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters")
rootCmd.Flags().BoolVarP(&config.QuietLogging, "quiet", "q", false, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&config.VerboseLogging, "verbose", "v", false, "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")
rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-ssl-cert", config.SMTPTLSCert, "SSL certificate for SMTP - requires smtp-ssl-key")
rootCmd.Flags().StringVar(&config.SMTPTLSKey, "smtp-ssl-key", config.SMTPTLSKey, "SSL key for SMTP - requires smtp-ssl-cert")
rootCmd.Flags().Lookup("ui-ssl-cert").Hidden = true
rootCmd.Flags().Lookup("ui-ssl-cert").Deprecated = "use --ui-tls-cert"
rootCmd.Flags().Lookup("ui-ssl-key").Hidden = true
rootCmd.Flags().Lookup("ui-ssl-key").Deprecated = "use --ui-tls-key"
rootCmd.Flags().Lookup("smtp-ssl-cert").Hidden = true
rootCmd.Flags().Lookup("smtp-ssl-cert").Deprecated = "use --smtp-tls-cert"
rootCmd.Flags().Lookup("smtp-ssl-key").Hidden = true
rootCmd.Flags().Lookup("smtp-ssl-key").Deprecated = "use --smtp-tls-key"
}
// 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")
@@ -85,73 +147,102 @@ func init() {
if len(os.Getenv("MP_MAX_MESSAGES")) > 0 {
config.MaxMessages, _ = strconv.Atoi(os.Getenv("MP_MAX_MESSAGES"))
}
if len(os.Getenv("MP_TAG")) > 0 {
config.SMTPCLITags = os.Getenv("MP_TAG")
}
// UI
if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 {
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
}
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
config.UISSLCert = os.Getenv("MP_UI_SSL_CERT")
if len(os.Getenv("MP_UI_TLS_CERT")) > 0 {
config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
}
if len(os.Getenv("MP_UI_SSL_KEY")) > 0 {
config.UISSLKey = os.Getenv("MP_UI_SSL_KEY")
if len(os.Getenv("MP_UI_TLS_KEY")) > 0 {
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_SSL_CERT")) > 0 {
config.SMTPSSLCert = os.Getenv("MP_SMTP_SSL_CERT")
if len(os.Getenv("MP_SMTP_TLS_CERT")) > 0 {
config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
}
if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 {
config.SMTPSSLKey = os.Getenv("MP_SMTP_SSL_KEY")
if len(os.Getenv("MP_SMTP_TLS_KEY")) > 0 {
config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
}
if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") {
config.SMTPAuthAcceptAny = true
}
if getEnabledFromEnv("MP_SMTP_AUTH_ALLOW_INSECURE") {
config.SMTPAuthAllowInsecure = true
}
if len(os.Getenv("MP_WEBROOT")) > 0 {
config.Webroot = os.Getenv("MP_WEBROOT")
}
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
config.UseMessageDates = true
}
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
config.UseMessageDates = true
}
if getEnabledFromEnv("MP_QUIET") {
config.QuietLogging = true
}
if getEnabledFromEnv("MP_VERBOSE") {
config.VerboseLogging = true
}
}
// 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 {
config.UISSLCert = os.Getenv("MP_SSL_CERT")
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 {
config.UISSLKey = os.Getenv("MP_SSL_KEY")
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("MP_DATA_DIR has been deprecated, use MP_DATA_FILE")
fmt.Println("ENV MP_DATA_DIR has been deprecated, use MP_DATA_FILE")
config.DataFile = os.Getenv("MP_DATA_DIR")
}
rootCmd.Flags().StringVarP(&config.DataFile, "db-file", "d", config.DataFile, "Database file to store persistent data")
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI authentication")
rootCmd.Flags().StringVar(&config.UISSLCert, "ui-ssl-cert", config.UISSLCert, "SSL certificate for web UI - requires ui-ssl-key")
rootCmd.Flags().StringVar(&config.UISSLKey, "ui-ssl-key", config.UISSLKey, "SSL key for web UI - requires ui-ssl-cert")
rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication")
rootCmd.Flags().StringVar(&config.SMTPSSLCert, "smtp-ssl-cert", config.SMTPSSLCert, "SSL certificate for SMTP - requires smtp-ssl-key")
rootCmd.Flags().StringVar(&config.SMTPSSLKey, "smtp-ssl-key", config.SMTPSSLKey, "SSL key for SMTP - requires smtp-ssl-cert")
rootCmd.Flags().BoolVarP(&config.QuietLogging, "quiet", "q", false, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&config.VerboseLogging, "verbose", "v", false, "Verbose logging")
// deprecated 2022/08/06
rootCmd.Flags().StringVarP(&config.UIAuthFile, "auth-file", "a", config.UIAuthFile, "A password file for web UI authentication")
rootCmd.Flags().StringVar(&config.UISSLCert, "ssl-cert", config.UISSLCert, "SSL certificate - requires ssl-key")
rootCmd.Flags().StringVar(&config.UISSLKey, "ssl-key", config.UISSLKey, "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-ssl-cert"
rootCmd.Flags().Lookup("ssl-key").Hidden = true
rootCmd.Flags().Lookup("ssl-key").Deprecated = "use --ui-ssl-key"
// deprecated 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 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")
}
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")
}
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")
}
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")
}
}
// Wrapper to get a boolean from an environment variable
func getEnabledFromEnv(k string) bool {
if len(os.Getenv(k)) > 0 {
v := strings.ToLower(os.Getenv(k))
return v == "1" || v == "true" || v == "yes"
}
return false
}

View File

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

View File

@@ -1,12 +1,16 @@
// Package config handles the application configuration
package config
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/mattn/go-shellwords"
"github.com/tg123/go-htpasswd"
)
@@ -23,6 +27,9 @@ var (
// MaxMessages is the maximum number of messages a mailbox can have (auto-pruned every minute)
MaxMessages = 500
// UseMessageDates sets the Created date using the message date, not the delivered date
UseMessageDates bool
// VerboseLogging for console output
VerboseLogging = false
@@ -32,11 +39,11 @@ var (
// NoLogging for tests
NoLogging = false
// UISSLCert file
UISSLCert string
// UITLSCert file
UITLSCert string
// UISSLKey file
UISSLKey string
// UITLSKey file
UITLSKey string
// UIAuthFile for basic authentication
UIAuthFile string
@@ -44,11 +51,14 @@ var (
// UIAuth used for euthentication
UIAuth *htpasswd.File
// SMTPSSLCert file
SMTPSSLCert string
// Webroot to define the base path for the UI and API
Webroot = "/"
// SMTPSSLKey file
SMTPSSLKey string
// SMTPTLSCert file
SMTPTLSCert string
// SMTPTLSKey file
SMTPTLSKey string
// SMTPAuthFile for SMTP authentication
SMTPAuthFile string
@@ -56,6 +66,21 @@ var (
// SMTPAuth used for euthentication
SMTPAuth *htpasswd.File
// SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication
SMTPAuthAllowInsecure bool
// SMTPAuthAcceptAny accepts any username/password including none
SMTPAuthAcceptAny bool
// SMTPCLITags is used to map the CLI args
SMTPCLITags string
// TagRegexp is the allowed tag characters
TagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`)
// SMTPTags are expressions to apply tags to new mail
SMTPTags []Tag
// ContentSecurityPolicy for HTTP server
ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';"
@@ -69,6 +94,12 @@ var (
RepoBinaryName = "mailpit"
)
// Tag struct
type Tag struct {
Tag string
Match string
}
// VerifyConfig wil do some basic checking
func VerifyConfig() error {
if DataFile != "" && isDir(DataFile) {
@@ -95,31 +126,31 @@ func VerifyConfig() error {
UIAuth = a
}
if UISSLCert != "" && UISSLKey == "" || UISSLCert == "" && UISSLKey != "" {
return errors.New("you must provide both a UI SSL certificate and a key")
if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" {
return errors.New("You must provide both a UI TLS certificate and a key")
}
if UISSLCert != "" {
if !isFile(UISSLCert) {
return fmt.Errorf("SSL certificate not found: %s", UISSLCert)
if UITLSCert != "" {
if !isFile(UITLSCert) {
return fmt.Errorf("TLS certificate not found: %s", UITLSCert)
}
if !isFile(UISSLKey) {
return fmt.Errorf("SSL key not found: %s", UISSLKey)
if !isFile(UITLSKey) {
return fmt.Errorf("TLS key not found: %s", UITLSKey)
}
}
if SMTPSSLCert != "" && SMTPSSLKey == "" || SMTPSSLCert == "" && SMTPSSLKey != "" {
return errors.New("you must provide both an SMTP SSL certificate and a key")
if SMTPTLSCert != "" && SMTPTLSKey == "" || SMTPTLSCert == "" && SMTPTLSKey != "" {
return errors.New("You must provide both an SMTP TLS certificate and a key")
}
if SMTPSSLCert != "" {
if !isFile(SMTPSSLCert) {
return fmt.Errorf("SMTP SSL certificate not found: %s", SMTPSSLCert)
if SMTPTLSCert != "" {
if !isFile(SMTPTLSCert) {
return fmt.Errorf("SMTP TLS certificate not found: %s", SMTPTLSCert)
}
if !isFile(SMTPSSLKey) {
return fmt.Errorf("SMTP SSL key not found: %s", SMTPSSLKey)
if !isFile(SMTPTLSKey) {
return fmt.Errorf("SMTP TLS key not found: %s", SMTPTLSKey)
}
}
@@ -128,8 +159,8 @@ func VerifyConfig() error {
return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile)
}
if SMTPSSLCert == "" {
return errors.New("SMTP authentication requires SMTP encryption")
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)
@@ -139,6 +170,47 @@ func VerifyConfig() error {
SMTPAuth = a
}
if SMTPTLSCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
return errors.New("SMTP authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
}
validWebrootRe := regexp.MustCompile(`[^0-9a-zA-Z\/\-\_\.]`)
if validWebrootRe.MatchString(Webroot) {
return fmt.Errorf("Invalid characters in Webroot (%s). Valid chars include: [a-z A-Z 0-9 _ . - /]", Webroot)
}
s := strings.TrimRight(path.Join("/", Webroot, "/"), "/") + "/"
Webroot = s
SMTPTags = []Tag{}
p := shellwords.NewParser()
if SMTPCLITags != "" {
args, err := p.Parse(SMTPCLITags)
if err != nil {
return fmt.Errorf("Error parsing tags (%s)", err)
}
for _, a := range args {
t := strings.Split(a, "=")
if len(t) > 1 {
tag := strings.TrimSpace(t[0])
if !TagRegexp.MatchString(tag) || len(tag) == 0 {
return fmt.Errorf("Invalid tag (%s) - can only contain spaces, letters, numbers, - & _", tag)
}
match := strings.TrimSpace(strings.ToLower(strings.Join(t[1:], "=")))
if len(match) == 0 {
return fmt.Errorf("Invalid tag match (%s) - no search detected", tag)
}
SMTPTags = append(SMTPTags, Tag{Tag: tag, Match: match})
} else {
return fmt.Errorf("Error parsing tags (%s)", a)
}
}
}
return nil
}

View File

@@ -26,8 +26,8 @@ Returns a JSON summary of the message and attachments.
"Address": "jane@example.com"
}
],
"Cc": null,
"Bcc": null,
"Cc": [],
"Bcc": [],
"Subject": "Message subject",
"Date": "2016-09-07T16:46:00+13:00",
"Text": "Plain text MIME part of the email",
@@ -57,7 +57,7 @@ Returns a JSON summary of the message and attachments.
- `Read` - always true (message marked read on open)
- `From` - Name & Address, or null
- `To`, `CC`, `BCC` - Array of Names & Address, or null
- `To`, `CC`, `BCC` - Array of Names & Address
- `Date` - Parsed email local date & time from headers
- `Size` - Total size of raw email
- `Inline`, `Attachments` - Array of attachments and inline images.

View File

@@ -51,7 +51,7 @@ List messages in the mailbox. Messages are returned in the order of latest recei
"Address": "accounts@example.com"
}
],
"Bcc": null,
"Bcc": [],
"Subject": "Message subject",
"Created": "2022-10-03T21:35:32.228605299+13:00",
"Size": 6144,
@@ -70,7 +70,7 @@ List messages in the mailbox. Messages are returned in the order of latest recei
- `start` - The offset (default `0`) for pagination
- `Read` - The read/unread status of the message
- `From` - Name & Address, or null if none
- `To`, `CC`, `BCC` - Array of Names & Address, or null if none
- `To`, `CC`, `BCC` - Array of Names & Address
- `Created` - Local date & time the message was received
- `Size` - Total size of raw email in bytes

View File

@@ -8,4 +8,5 @@ The API is split into three main parts:
- [Messages](Messages.md) - Listing, deleting & marking messages as read/unread.
- [Message](Message.md) - Return message data & attachments
- [Tags](Tags.md) - Set message tags
- [Search](Search.md) - Searching messages

View File

@@ -47,7 +47,7 @@ Matching messages are returned in the order of latest received to oldest.
"Address": "accounts@example.com"
}
],
"Bcc": null,
"Bcc": [],
"Subject": "Test email",
"Created": "2022-10-03T21:35:32.228605299+13:00",
"Size": 6144,
@@ -65,5 +65,5 @@ Matching messages are returned in the order of latest received to oldest.
- `count` - Number of messages returned in request
- `start` - The offset (default `0`) for pagination
- `From` - Singular Name & Address, or null if none
- `To`, `CC`, `BCC` - Array of Name & Address, or null if none
- `To`, `CC`, `BCC` - Array of Name & Address
- `Size` - Total size of raw email in bytes

27
docs/apiv1/Tags.md Normal file
View File

@@ -0,0 +1,27 @@
# Tags
Set message tags.
---
## Update message tags
Set the tags for one or more messages.
If the tags array is empty then all tags are removed from the messages.
**URL** : `api/v1/tags`
**Method** : `PUT`
### Request
```json
{
"ids": ["<ID>","<ID>"...],
"tags": ["<tag>","<tag>"]
}
```
### Response
**Status** : `200`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -1,14 +1,13 @@
const { build } = require('esbuild')
const pluginVue = require('esbuild-plugin-vue-next')
const { sassPlugin } = require('esbuild-sass-plugin');
import * as esbuild from 'esbuild'
import pluginVue from 'esbuild-plugin-vue-next'
import { sassPlugin } from 'esbuild-sass-plugin'
const doWatch = process.env.WATCH == 'true' ? true : false;
const doMinify = process.env.MINIFY == 'true' ? true : false;
build({
const ctx = await esbuild.context({
entryPoints: ["server/ui-src/app.js"],
bundle: true,
watch: doWatch,
minify: doMinify,
sourcemap: false,
outfile: "server/ui/dist/app.js",
@@ -20,3 +19,10 @@ build({
},
logLevel: "info"
})
if (doWatch) {
await ctx.watch()
} else {
await ctx.rebuild()
ctx.dispose()
}

44
go.mod
View File

@@ -8,19 +8,19 @@ require (
github.com/disintegration/imaging v1.6.2
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/jhillyerd/enmime v0.10.1
github.com/k3a/html2text v1.0.8
github.com/klauspost/compress v1.15.11
github.com/jhillyerd/enmime v0.11.0
github.com/k3a/html2text v1.1.0
github.com/klauspost/compress v1.16.0
github.com/leporo/sqlf v1.3.0
github.com/mattn/go-shellwords v1.0.12
github.com/mhale/smtpd v0.8.0
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.5.0
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/tg123/go-htpasswd v1.2.0
golang.org/x/text v0.3.7
modernc.org/sqlite v1.19.1
github.com/tg123/go-htpasswd v1.2.1
golang.org/x/text v0.7.0
modernc.org/sqlite v1.21.0
)
require (
@@ -28,35 +28,35 @@ require (
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
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.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba // 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.16 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.7.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b // indirect
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/image v0.5.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/tools v0.6.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.9 // indirect
modernc.org/libc v1.20.2 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
modernc.org/token v1.1.0 // indirect
)

139
go.sum
View File

@@ -35,8 +35,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
@@ -45,8 +45,8 @@ github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW
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/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -55,22 +55,22 @@ 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/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.10.1 h1:3VP8gFhK7R948YJBrna5bOgnTXEuPAoICo79kKkBKfA=
github.com/jhillyerd/enmime v0.10.1/go.mod h1:Qpe8EEemJMFAF8+NZoWdpXvK2Yb9dRF0k/z6mkcDHsA=
github.com/jhillyerd/enmime v0.11.0 h1:5EOSLh7l3eMODznfMCKVGQY74Qb95Yfuet6CYgquLfM=
github.com/jhillyerd/enmime v0.11.0/go.mod h1:nw2aJ34YXWklLze+qEESgP+KNhU3fMQuiFsD/4soh3Q=
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.0.8 h1:rVanLhKilpnJUJs/CNKWzMC4YaQINGxK0rSG8ssmnV0=
github.com/k3a/html2text v1.0.8/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
github.com/k3a/html2text v1.1.0 h1:ks4hKSTdiTRsLr0DM771mI5TvsoG6zH7m1Ulv7eJRHw=
github.com/k3a/html2text v1.1.0/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.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
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=
@@ -80,9 +80,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.3.0 h1:nAkuPYxMIJg/sUmcd1h4avV5iYo8tBTGEGOIR4BIZO8=
github.com/leporo/sqlf v1.3.0/go.mod h1:f4dHqIi1+nLl6k1IsNQ8QIEbGWK49th2ei1IxTXk+2E=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
@@ -90,7 +89,7 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mhale/smtpd v0.8.0 h1:5JvdsehCg33PQrZBvFyDMMUDQmvbzVpZgKob7eYBJc0=
github.com/mhale/smtpd v0.8.0/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@@ -100,12 +99,12 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa h1:tEkEyxYeZ43TR55QU/hsIt9aRGBxbgGuz9CGykjvogY=
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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=
@@ -117,110 +116,92 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
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.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
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=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0=
github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU=
github.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM=
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
modernc.org/libc v1.20.2 h1:9/C6hYLe+SNLricCd+WYkIGatWrQTZegOfmOcz5fPmY=
modernc.org/libc v1.20.2/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.19.1 h1:8xmS5oLnZtAK//vnd4aTVj8VOeTAccEFOtUnIzfSw+4=
modernc.org/sqlite v1.19.1/go.mod h1:UfQ83woKMaPW/ZBruK0T7YaFCrI+IE0LeWVY6pmnVms=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.14.0 h1:cO7oyRWEXweSJmjdbs1L86P52D9QmBy/CPFKmFvNYTU=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.6.0 h1:gLwAw6aS973K/k9EOJGlofauyMk4YOUiPDYzWnq/oXo=
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=

View File

@@ -7,7 +7,7 @@ set -e
VERSION=$(curl --silent --location --max-time "${TIMEOUT}" "https://api.github.com/repos/${GH_REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [ $? -ne 0 ]; then
echo -ne "\nThere was an error trying to check what is the latest version of ssbak.\nPlease try again later.\n"
echo -ne "\nThere was an error trying to check what is the latest version of Mailpit.\nPlease try again later.\n"
exit 1
fi

1559
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,15 @@
"version": "0.0.0",
"private": true,
"scripts": {
"build": "node esbuild.config.js",
"watch": "WATCH=true node esbuild.config.js",
"package": "MINIFY=true node esbuild.config.js"
"build": "node esbuild.config.mjs",
"watch": "WATCH=true node esbuild.config.mjs",
"package": "MINIFY=true node esbuild.config.mjs"
},
"dependencies": {
"axios": "^0.27.2",
"axios": "^1.2.1",
"bootstrap": "^5.2.0",
"bootstrap-icons": "^1.9.1",
"bootstrap5-tags": "^1.4.41",
"moment": "^2.29.4",
"prismjs": "^1.29.0",
"tinycon": "^0.6.8",
@@ -19,7 +20,7 @@
"devDependencies": {
"@popperjs/core": "^2.11.5",
"@vue/compiler-sfc": "^3.2.37",
"esbuild": "^0.14.50",
"esbuild": "^0.17.5",
"esbuild-plugin-vue-next": "^0.1.4",
"esbuild-sass-plugin": "^2.3.2"
}

View File

@@ -13,7 +13,7 @@ import (
"os"
"os/user"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/utils/logger"
flag "github.com/spf13/pflag"
)

View File

@@ -1,6 +1,7 @@
package apiv1
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
@@ -13,17 +14,8 @@ import (
"github.com/gorilla/mux"
)
// MessagesResult struct
type MessagesResult struct {
Total int `json:"total"`
Unread int `json:"unread"`
Count int `json:"count"`
Start int `json:"start"`
Messages []storage.Summary `json:"messages"`
}
// Messages returns a paginated list of messages as JSON
func Messages(w http.ResponseWriter, r *http.Request) {
// GetMessages returns a paginated list of messages as JSON
func GetMessages(w http.ResponseWriter, r *http.Request) {
start, limit := getStartLimit(r)
messages, err := storage.List(start, limit)
@@ -34,13 +26,14 @@ func Messages(w http.ResponseWriter, r *http.Request) {
stats := storage.StatsGet()
var res MessagesResult
var res MessagesSummary
res.Start = start
res.Messages = messages
res.Count = len(messages)
res.Total = stats.Total
res.Unread = stats.Unread
res.Tags = stats.Tags
bytes, _ := json.Marshal(res)
w.Header().Add("Content-Type", "application/json")
@@ -65,21 +58,22 @@ func Search(w http.ResponseWriter, r *http.Request) {
stats := storage.StatsGet()
var res MessagesResult
var res MessagesSummary
res.Start = 0
res.Messages = messages
res.Count = len(messages)
res.Total = stats.Total
res.Unread = stats.Unread
res.Tags = stats.Tags
bytes, _ := json.Marshal(res)
w.Header().Add("Content-Type", "application/json")
_, _ = w.Write(bytes)
}
// Message (method: GET) returns the *data.Message as JSON
func Message(w http.ResponseWriter, r *http.Request) {
// GetMessage (method: GET) returns the *data.Message as JSON
func GetMessage(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
@@ -129,7 +123,7 @@ func Headers(w http.ResponseWriter, r *http.Request) {
return
}
reader := strings.NewReader(string(data))
reader := bytes.NewReader(data)
m, err := mail.ReadMessage(reader)
if err != nil {
httpError(w, err.Error())
@@ -243,6 +237,36 @@ func SetReadStatus(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
}
// SetTags (method: PUT) will set the tags for all provided IDs
func SetTags(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var data struct {
Tags []string
IDs []string
}
err := decoder.Decode(&data)
if err != nil {
httpError(w, err.Error())
return
}
ids := data.IDs
if len(ids) > 0 {
for _, id := range ids {
if err := storage.SetTags(id, data.Tags); err != nil {
httpError(w, err.Error())
return
}
}
}
w.Header().Add("Content-Type", "text/plain")
_, _ = w.Write([]byte("ok"))
}
// FourOFour returns a basic 404 message
func fourOFour(w http.ResponseWriter) {
w.Header().Set("Referrer-Policy", "no-referrer")

View File

@@ -8,7 +8,7 @@ import (
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/updater"
"github.com/axllent/mailpit/utils/updater"
)
type appVersion struct {
@@ -26,6 +26,11 @@ func AppInfo(w http.ResponseWriter, r *http.Request) {
info := appVersion{}
info.Version = config.Version
var m runtime.MemStats
runtime.ReadMemStats(&m)
info.Memory = m.Sys - m.HeapReleased
latest, _, _, err := updater.GithubLatest(config.Repo, config.RepoBinaryName)
if err == nil {
info.LatestVersion = latest
@@ -40,11 +45,6 @@ func AppInfo(w http.ResponseWriter, r *http.Request) {
info.Messages = storage.CountTotal()
var m runtime.MemStats
runtime.ReadMemStats(&m)
info.Memory = m.Sys - m.HeapReleased
bytes, _ := json.Marshal(info)
w.Header().Add("Content-Type", "application/json")

25
server/apiv1/structs.go Normal file
View File

@@ -0,0 +1,25 @@
package apiv1
import "github.com/axllent/mailpit/storage"
// The following structs & aliases are provided for easy import
// and understanding of the JSON structure.
// MessageSummary - summary of a single message
type MessageSummary = storage.MessageSummary
// MessagesSummary - summary of a list of messages
type MessagesSummary struct {
Total int `json:"total"`
Unread int `json:"unread"`
Count int `json:"count"`
Start int `json:"start"`
Tags []string `json:"tags"`
Messages []MessageSummary `json:"messages"`
}
// Message data
type Message = storage.Message
// Attachment summary
type Attachment = storage.Attachment

View File

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

View File

@@ -0,0 +1,8 @@
package handlers
import "net/http"
// Healthz is a liveness probe
func HealthzHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}

View File

@@ -0,0 +1,17 @@
package handlers
import (
"net/http"
"sync/atomic"
)
// ReadyzHandler is a ready probe that signals k8s to be able to retrieve traffic
func ReadyzHandler(isReady *atomic.Value) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
if isReady == nil || !isReady.Load().(bool) {
http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}
}

View File

@@ -8,11 +8,13 @@ import (
"net/http"
"os"
"strings"
"sync/atomic"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/server/apiv1"
"github.com/axllent/mailpit/server/handlers"
"github.com/axllent/mailpit/server/websockets"
"github.com/axllent/mailpit/utils/logger"
"github.com/gorilla/mux"
)
@@ -21,6 +23,9 @@ var embeddedFS embed.FS
// Listen will start the httpd
func Listen() {
isReady := &atomic.Value{}
isReady.Store(false)
serverRoot, err := fs.Sub(embeddedFS, "ui")
if err != nil {
logger.Log().Errorf("[http] %s", err)
@@ -33,40 +38,56 @@ func Listen() {
r := defaultRoutes()
// kubernetes probes
r.HandleFunc("/livez", handlers.HealthzHandler)
r.HandleFunc("/readyz", handlers.ReadyzHandler(isReady))
// web UI websocket
r.HandleFunc("/api/events", apiWebsocket).Methods("GET")
r.HandleFunc(config.Webroot+"api/events", apiWebsocket).Methods("GET")
// virtual filesystem for others
r.PathPrefix("/").Handler(middlewareHandler(http.FileServer(http.FS(serverRoot))))
r.PathPrefix(config.Webroot).Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot)))))
// redirect to webroot if no trailing slash
if config.Webroot != "/" {
redir := strings.TrimRight(config.Webroot, "/")
r.HandleFunc(redir, middleWareFunc(addSlashToWebroot)).Methods("GET")
}
http.Handle("/", r)
if config.UIAuthFile != "" {
logger.Log().Info("[http] enabling web UI basic authentication")
}
if config.UISSLCert != "" && config.UISSLKey != "" {
logger.Log().Infof("[http] starting secure server on https://%s", config.HTTPListen)
logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UISSLCert, config.UISSLKey, nil))
// Mark the application here as ready
isReady.Store(true)
if config.UITLSCert != "" && config.UITLSKey != "" {
logger.Log().Infof("[http] starting secure server on https://%s%s", 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", config.HTTPListen)
logger.Log().Infof("[http] starting server on http://%s%s", config.HTTPListen, config.Webroot)
logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil))
}
}
func defaultRoutes() *mux.Router {
r := mux.NewRouter()
// API V1
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.Messages)).Methods("GET")
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT")
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
r.HandleFunc("/api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/headers", middleWareFunc(apiv1.Headers)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}", middleWareFunc(apiv1.Message)).Methods("GET")
r.HandleFunc("/api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.GetMessages)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT")
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetTags)).Methods("PUT")
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.Headers)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
return r
}
@@ -152,6 +173,11 @@ func middlewareHandler(h http.Handler) http.Handler {
})
}
// Redirect to webroot
func addSlashToWebroot(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, config.Webroot, http.StatusFound)
}
// Websocket to broadcast changes
func apiWebsocket(w http.ResponseWriter, r *http.Request) {
websockets.ServeWs(websockets.MessageHub, w, r)

View File

@@ -160,7 +160,7 @@ func setup() {
}
func assertStatsEqual(t *testing.T, uri string, unread, total int) {
m := apiv1.MessagesResult{}
m := apiv1.MessagesSummary{}
data, err := clientGet(uri)
if err != nil {
@@ -179,7 +179,7 @@ func assertStatsEqual(t *testing.T, uri string, unread, total int) {
func assertSearchEqual(t *testing.T, uri, query string, count int) {
t.Logf("Test search: %s", query)
m := apiv1.MessagesResult{}
m := apiv1.MessagesSummary{}
limit := fmt.Sprintf("%d", count)
@@ -226,8 +226,8 @@ func insertEmailData(t *testing.T) {
}
func fetchMessages(url string) (apiv1.MessagesResult, error) {
m := apiv1.MessagesResult{}
func fetchMessages(url string) (apiv1.MessagesSummary, error) {
m := apiv1.MessagesSummary{}
data, err := clientGet(url)
if err != nil {

115
server/smtpd/smtpd.go Normal file
View File

@@ -0,0 +1,115 @@
// Package smtpd is the SMTP daemon
package smtpd
import (
"bytes"
"net"
"net/mail"
"regexp"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/mhale/smtpd"
)
func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
msg, err := mail.ReadMessage(bytes.NewReader(data))
if err != nil {
logger.Log().Errorf("error parsing message: %s", err.Error())
return err
}
if _, err := storage.Store(data); err != nil {
// Value with size 4800709 exceeded 1048576 limit
re := regexp.MustCompile(`(Value with size \d+ exceeded \d+ limit)`)
tooLarge := re.FindStringSubmatch(err.Error())
if len(tooLarge) > 0 {
logger.Log().Errorf("[db] error storing message: %s", tooLarge[0])
} else {
logger.Log().Errorf("[db] error storing message")
logger.Log().Errorf(err.Error())
}
return err
}
subject := msg.Header.Get("Subject")
logger.Log().Debugf("[smtp] received (%s) from:%s to:%s subject:%q", cleanIP(origin), from, to[0], subject)
return nil
}
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
allow := config.SMTPAuth.Match(string(username), string(password))
if allow {
logger.Log().Debugf("[smtp] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
} else {
logger.Log().Warnf("[smtp] deny %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
}
return allow, nil
}
// Allow any username and password
func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
logger.Log().Debugf("[smtp] allow %s login %q from %s", mechanism, string(username), cleanIP(remoteAddr))
return true, nil
}
// Listen starts the SMTPD server
func Listen() error {
if config.SMTPAuthAllowInsecure {
if config.SMTPAuthFile != "" {
logger.Log().Infof("[smtp] enabling login auth via %s (insecure)", config.SMTPAuthFile)
} else if config.SMTPAuthAcceptAny {
logger.Log().Info("[smtp] enabling all auth (insecure)")
}
} else {
if config.SMTPAuthFile != "" {
logger.Log().Infof("[smtp] enabling login auth via %s (TLS)", config.SMTPAuthFile)
} else if config.SMTPAuthAcceptAny {
logger.Log().Info("[smtp] enabling any auth (TLS)")
}
}
logger.Log().Infof("[smtp] starting on %s", config.SMTPListen)
return listenAndServe(config.SMTPListen, mailHandler, authHandler)
}
func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHandler) error {
srv := &smtpd.Server{
Addr: addr,
Handler: handler,
Appname: "Mailpit",
Hostname: "",
AuthHandler: nil,
AuthRequired: false,
}
if config.SMTPAuthAllowInsecure {
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
}
if config.SMTPAuthFile != "" {
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
srv.AuthHandler = authHandler
srv.AuthRequired = true
} else if config.SMTPAuthAcceptAny {
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
srv.AuthHandler = authHandlerAny
}
if config.SMTPTLSCert != "" {
if err := srv.ConfigureTLS(config.SMTPTLSCert, config.SMTPTLSKey); err != nil {
return err
}
}
return srv.ListenAndServe()
}
func cleanIP(i net.Addr) string {
parts := strings.Split(i.String(), ":")
return parts[0]
}

View File

@@ -1,14 +1,18 @@
<script>
import commonMixins from './mixins.js';
import Message from './templates/Message.vue';
import MessageSummary from './templates/MessageSummary.vue';
import moment from 'moment';
import Tinycon from 'tinycon';
export default {
mixins: [commonMixins],
components: {
Message
Message,
MessageSummary
},
data() {
return {
currentPath: window.location.hash,
@@ -18,6 +22,8 @@ export default {
unread: 0,
start: 0,
count: 0,
tags: [],
existingTags: [], // to pass onto components
search: "",
searching: false,
isConnected: false,
@@ -30,9 +36,10 @@ export default {
selected: [],
tcStatus: 0,
appInfo: false,
lastLoaded: false,
lastLoaded: false
}
},
watch: {
currentPath(v, old) {
if (v && v.match(/^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/)) {
@@ -53,6 +60,7 @@ export default {
}
}
},
computed: {
canPrev: function () {
return this.start > 0;
@@ -61,6 +69,7 @@ export default {
return this.total > (this.start + this.count);
}
},
mounted() {
this.currentPath = window.location.hash.slice(1);
window.addEventListener('hashchange', () => {
@@ -80,19 +89,21 @@ export default {
this.connect();
this.loadMessages();
},
methods: {
loadMessages: function () {
let now = Date.now()
// prevent double loading when websocket connects
// prevent double loading when UI loads & websocket connects
if (this.lastLoaded && now - this.lastLoaded < 250) {
return;
}
this.lastLoaded = now;
if (this.start == 0) {
this.lastLoaded = now;
}
let self = this;
let params = {};
this.selected = [];
self.selected = [];
let uri = 'api/v1/messages';
if (self.search) {
@@ -116,8 +127,12 @@ export default {
self.count = response.data.count;
self.start = response.data.start;
self.items = response.data.messages;
if (self.items == 0 && self.start > 0) {
self.tags = response.data.tags;
if (!self.existingTags.length) {
self.existingTags = JSON.parse(JSON.stringify(self.tags));
}
// if pagination > 0 && results == 0 reload first page (prune)
if (response.data.count == 0 && response.data.start > 0) {
self.start = 0;
return self.loadMessages();
}
@@ -129,7 +144,7 @@ export default {
}
}
self.scrollInPlace = false
self.scrollInPlace = false;
});
},
@@ -138,6 +153,16 @@ export default {
this.loadMessages();
},
tagSearch: function (e, tag) {
e.preventDefault();
if (tag.match(/ /)) {
tag = '"' + tag + '"';
}
this.search = 'tag:' + tag;
window.location.hash = "";
this.loadMessages();
},
resetSearch: function (e) {
e.preventDefault();
this.search = '';
@@ -168,9 +193,11 @@ export default {
openMessage: function (id) {
let self = this;
self.selected = [];
self.existingTags = JSON.parse(JSON.stringify(self.tags));
let uri = 'api/v1/message/' + self.currentPath
self.get(uri, false, function (response) {
for (let i in self.items) {
if (self.items[i].ID == self.currentPath) {
if (!self.items[i].Read) {
@@ -187,14 +214,14 @@ export default {
if (a.ContentID != '') {
d.HTML = d.HTML.replace(
new RegExp('cid:' + a.ContentID, 'g'),
window.location.origin + '/api/v1/message/' + d.ID + '/part/' + a.PartID
window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID
);
}
if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) {
// some old email clients use the filename
d.HTML = d.HTML.replace(
new RegExp('src=(\'|")' + a.FileName + '(\'|")', 'g'),
'src="' + window.location.origin + '/api/v1/message/' + d.ID + '/part/' + a.PartID + '"'
'src="' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '"'
);
}
}
@@ -206,14 +233,14 @@ export default {
if (a.ContentID != '') {
d.HTML = d.HTML.replace(
new RegExp('cid:' + a.ContentID, 'g'),
window.location.origin + '/api/v1/message/' + d.ID + '/part/' + a.PartID
window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID
);
}
if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) {
// some old email clients use the filename
d.HTML = d.HTML.replace(
new RegExp('src=(\'|")' + a.FileName + '(\'|")', 'g'),
'src="' + window.location.origin + '/api/v1/message/' + d.ID + '/part/' + a.PartID + '"'
'src="' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '"'
);
}
}
@@ -367,6 +394,14 @@ export default {
}
self.total++;
self.unread++;
for (let i in response.Data.Tags) {
if (self.tags.indexOf(response.Data.Tags[i]) < 0) {
self.tags.push(response.Data.Tags[i]);
self.tags.sort();
}
}
let from = response.Data.From != null ? response.Data.From.Address : '[unknown]';
self.browserNotify("New mail from: " + from, response.Data.Subject);
} else if (response.Type == "prune") {
@@ -416,7 +451,7 @@ export default {
let b = message.Subject;
let options = {
body: message,
icon: 'mailpit.png'
icon: 'notification.png'
}
new Notification(title, options);
}
@@ -462,7 +497,7 @@ export default {
this.selected = this.selected.filter(function (ele) {
return ele != id;
});
return
return;
}
if (lastSelected === false) {
@@ -492,6 +527,15 @@ export default {
return this.selected.indexOf(id) != -1;
},
inSearch: function (tag) {
tag = tag.toLowerCase();
if (tag.match(/ /)) {
tag = '"' + tag + '"';
}
return this.search.toLowerCase().indexOf('tag:' + tag) > -1;
},
loadInfo: function (e) {
e.preventDefault();
let self = this;
@@ -505,64 +549,64 @@ export default {
</script>
<template>
<div class="navbar navbar-expand-lg navbar-light row flex-shrink-0 bg-light shadow-sm">
<div class="navbar navbar-expand-lg navbar-dark row flex-shrink-0 bg-primary text-white">
<div class="col-lg-2 col-md-3 d-none d-md-block">
<a class="navbar-brand" href="#" v-on:click="reloadMessages">
<a class="navbar-brand text-white" href="#" v-on:click="reloadMessages">
<img src="mailpit.svg" alt="Mailpit">
<span class="ms-2">Mailpit</span>
</a>
</div>
<div class="col col-md-9 col-lg-10" v-if="message">
<a class="btn btn-outline-secondary me-4 px-3" href="#" v-on:click="message=false"
<a class="btn btn-outline-light me-4 px-3 d-md-none" href="#" v-on:click="message = false"
title="Return to messages">
<i class="bi bi-arrow-return-left"></i>
</a>
<button class="btn btn-outline-secondary me-2" title="Mark unread" v-on:click="markUnread">
<button class="btn btn-outline-light me-2" title="Mark unread" v-on:click="markUnread">
<i class="bi bi-eye-slash"></i> <span class="d-none d-md-inline">Mark unread</span>
</button>
<button class="btn btn-outline-secondary me-2" title="Delete message" v-on:click="deleteMessages">
<button class="btn btn-outline-light me-2" title="Delete message" v-on:click="deleteMessages">
<i class="bi bi-trash-fill"></i> <span class="d-none d-md-inline">Delete</span>
</button>
<a class="btn btn-outline-secondary float-end" :class="messageNext ? '':'disabled'" :href="'#'+messageNext"
<a class="btn btn-outline-light float-end" :class="messageNext ? '' : 'disabled'" :href="'#' + messageNext"
title="View next message">
<i class="bi bi-caret-right-fill"></i>
</a>
<a class="btn btn-outline-secondary ms-2 me-1 float-end" :class="messagePrev ? '': 'disabled'"
:href="'#'+messagePrev" title="View previous message">
<a class="btn btn-outline-light ms-2 me-1 float-end" :class="messagePrev ? '' : 'disabled'"
:href="'#' + messagePrev" title="View previous message">
<i class="bi bi-caret-left-fill"></i>
</a>
<a :href="'api/v1/message/' + message.ID + '/raw?dl=1'" class="btn btn-outline-secondary me-2 float-end"
<a :href="'api/v1/message/' + message.ID + '/raw?dl=1'" class="btn btn-outline-light me-2 float-end"
title="Download message">
<i class="bi bi-file-arrow-down-fill"></i> <span class="d-none d-md-inline">Download</span>
</a>
</div>
<div class="col col-md-9 col-lg-5 LOL" v-if="!message">
<div class="col col-md-9 col-lg-5" v-if="!message">
<form v-on:submit="doSearch">
<div class="input-group">
<a class="navbar-brand d-md-none" href="#" v-on:click="reloadMessages">
<img src="mailpit.svg" alt="Mailpit">
<span v-if="!total" class="ms-2">Mailpit</span>
</a>
<div v-if="total" class="d-flex bg-white border rounded-start flex-fill position-relative">
<div v-if="total" class="ms-md-2 d-flex bg-white border rounded-start flex-fill position-relative">
<input type="text" class="form-control border-0" v-model.trim="search"
placeholder="Search mailbox">
<span class="btn btn-link position-absolute end-0 text-muted" v-if="search"
v-on:click="resetSearch"><i class="bi bi-x-circle"></i></span>
</div>
<button v-if="total" class="btn btn-outline-secondary" type="submit"><i
<button v-if="total" class="btn btn-outline-light" type="submit"><i
class="bi bi-search"></i></button>
</div>
</form>
</div>
<div class="col-12 col-lg-5 text-end mt-2 mt-lg-0" v-if="!message && total">
<button v-if="total" class="btn btn-outline-danger float-start d-md-none me-2" data-bs-toggle="modal"
<button v-if="total" class="btn btn-danger float-start d-md-none me-2" data-bs-toggle="modal"
data-bs-target="#DeleteAllModal" title="Delete all messages">
<i class="bi bi-trash-fill"></i>
</button>
<button v-if="unread" class="btn btn-outline-primary float-start d-md-none" data-bs-toggle="modal"
<button v-if="unread" class="btn btn-light float-start d-md-none" data-bs-toggle="modal"
data-bs-target="#MarkAllReadModal" title="Mark all read">
<i class="bi bi-check2-square"></i>
</button>
@@ -575,127 +619,143 @@ export default {
<option value="200">200</option>
</select>
<span v-if="searching">
<b>{{ formatNumber(items.length) }} results</b>
<b>{{ formatNumber(items.length) }} result<template v-if="items.length != 1">s</template></b>
</span>
<span v-else>
<small>
<b>{{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }}</b> of <b>{{
formatNumber(total) }}</b>
{{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} <small>of</small>
{{ formatNumber(total) }}
</small>
<button class="btn btn-outline-secondary ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev"
v-if="!searching" :title="'View previous '+limit+' messages'">
<button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev"
v-if="!searching" :title="'View previous ' + limit + ' messages'">
<i class="bi bi-caret-left-fill"></i>
</button>
<button class="btn btn-outline-secondary" :disabled="!canNext" v-on:click="viewNext" v-if="!searching"
:title="'View next '+limit+' messages'">
<button class="btn btn-outline-light" :disabled="!canNext" v-on:click="viewNext" v-if="!searching"
:title="'View next ' + limit + ' messages'">
<i class="bi bi-caret-right-fill"></i>
</button>
</span>
</div>
</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" style="overflow-y: auto;">
<ul class="list-unstyled mt-3 mb-5">
<li v-if="isConnected" title="Messages will auto-load" class="mb-3">
<i class="bi bi-power text-success"></i>
Connected
</li>
<li v-else title="You need to manually refresh your mailbox" class="mb-3">
<i class="bi bi-power text-danger"></i>
Disconnected
</li>
<li class="mb-5">
<a class="position-relative ps-0" href="#" v-on:click="reloadMessages">
<i class="bi bi-envelope me-1" v-if="isConnected"></i>
<i class="bi bi-arrow-clockwise me-1" v-else></i>
Inbox
<span class="badge rounded-pill text-bg-primary ms-1" title="Unread messages" v-if="unread">
{{ formatNumber(unread) }}
</span>
</a>
</li>
<li class="my-3" v-if="!message && unread && !selected.length">
<a href="#" data-bs-toggle="modal" data-bs-target="#MarkAllReadModal">
<div class="d-none d-md-block col-lg-2 col-md-3 mh-100 position-relative"
style="overflow-y: auto; overflow-x: hidden;">
<div class="list-group my-2">
<a href="#" v-on:click="message ? message = false : reloadMessages()"
class="list-group-item list-group-item-action" :class="!searching && !message ? 'active' : ''">
<template v-if="isConnected">
<i class="bi bi-envelope-fill me-1" v-if="!searching && !message"></i>
<i class="bi bi-arrow-return-left" v-else></i>
</template>
<i class="bi bi-arrow-clockwise me-1" v-else></i>
<span v-if="message" class="ms-1">Return</span>
<span v-else class="ms-1">Inbox</span>
<span class="badge rounded-pill ms-1 float-end text-bg-secondary" title="Unread messages">
{{ formatNumber(unread) }}
</span>
</a>
<template v-if="!message && !selected.length">
<button class="list-group-item list-group-item-action" data-bs-toggle="modal"
data-bs-target="#MarkAllReadModal" :disabled="!unread || searching">
<i class="bi bi-eye-fill"></i>
Mark all read
</a>
</li>
<li class="my-3" v-if="!message && total && !selected.length">
<a href="#" data-bs-toggle="modal" data-bs-target="#DeleteAllModal">
</button>
<button class="list-group-item list-group-item-action" data-bs-toggle="modal"
data-bs-target="#DeleteAllModal" :disabled="!total || searching">
<i class="bi bi-trash-fill me-1 text-danger"></i>
Delete all
</a>
</li>
<li class="my-3" v-if="selected.length > 0">
<b class="me-2">Selected {{selected.length}}</b>
<button class="btn btn-sm text-muted" v-on:click="selected=[]" title="Unselect messages"><i
class="bi bi-x-circle"></i></button>
</li>
<li class="my-3 ms-2" v-if="selected.length > 0 && selectedHasUnread()">
<a href="#" v-on:click="markSelectedRead">
<i class="bi bi-eye-fill"></i>
Mark read
</a>
</li>
<li class="my-3 ms-2" v-if="selected.length > 0 && selectedHasRead()">
<a href="#" v-on:click="markSelectedUnread">
<i class="bi bi-eye-slash"></i>
Mark unread
</a>
</li>
<li class="my-3 ms-2" v-if="total && selected.length > 0">
<a href="#" v-on:click="deleteMessages">
<i class="bi bi-trash-fill me-1 text-danger"></i>
Delete
</a>
</li>
<li class="my-3" v-if="notificationsSupported && !notificationsEnabled">
<a href="#" data-bs-toggle="modal" data-bs-target="#EnableNotificationsModal"
title="Enable browser notifications">
</button>
<button class="list-group-item list-group-item-action" data-bs-toggle="modal"
data-bs-target="#EnableNotificationsModal"
v-if="isConnected && notificationsSupported && !notificationsEnabled">
<i class="bi bi-bell"></i>
Enable alerts
</a>
</li>
<li class="mt-5 position-fixed bottom-0 bg-white py-2 text-muted">
<a href="#" class="text-muted" v-on:click="loadInfo">
<i class="bi bi-info-circle-fill"></i>
About
</a>
</li>
</ul>
</button>
</template>
<template v-if="!message && selected.length">
<button class="list-group-item list-group-item-action" :disabled="!selectedHasUnread()"
v-on:click="markSelectedRead">
<i class="bi bi-eye-fill"></i>
Mark selected read
</button>
<button class="list-group-item list-group-item-action" :disabled="!selectedHasRead()"
v-on:click="markSelectedUnread">
<i class="bi bi-eye-slash"></i>
Mark selected unread
</button>
<button class="list-group-item list-group-item-action" v-on:click="deleteMessages">
<i class="bi bi-trash-fill me-1 text-danger"></i>
Delete selected
</button>
<button class="list-group-item list-group-item-action" v-on:click="selected = []">
<i class="bi bi-x-circle me-1"></i>
Cancel selection
</button>
</template>
</div>
<template v-if="!selected.length && tags.length && !message">
<h6 class="mt-4 text-muted"><small>Tags</small></h6>
<div class="list-group mt-2 mb-5">
<button class="list-group-item list-group-item-action" v-for="tag in tags"
v-on:click="tagSearch($event, tag)" :class="inSearch(tag) ? 'active' : ''">
<i class="bi bi-tag-fill" v-if="inSearch(tag)"></i>
<i class="bi bi-tag" v-else></i>
{{ tag }}
</button>
</div>
</template>
<MessageSummary v-if="message" :message="message"></MessageSummary>
<div class="position-fixed bottom-0 bg-white py-2 text-muted w-100">
<a href="#" class="text-muted" v-on:click="loadInfo">
<i class="bi bi-info-circle-fill"></i>
About
</a>
</div>
</div>
<div class="col-lg-10 col-md-9 mh-100 pe-0">
<div class="mh-100" style="overflow-y: auto;" :class="message ? 'd-none':''" id="message-page">
<div class="list-group" v-if="items.length">
<a v-for="message in items" :href="'#'+message.ID"
<div class="mh-100" style="overflow-y: auto;" :class="message ? 'd-none' : ''" id="message-page">
<div class="list-group my-2" v-if="items.length">
<a v-for="message in items" :href="'#' + message.ID"
v-on:click.ctrl="toggleSelected($event, message.ID)"
v-on:click.shift="selectRange($event, message.ID)"
class="row message d-flex small list-group-item list-group-item-action border-start-0 border-end-0"
:class="message.Read ? 'read':'', isSelected(message.ID) ? 'selected':''">
:class="message.Read ? 'read' : '', isSelected(message.ID) ? 'selected' : ''">
<div class="col-lg-3">
<div class="d-lg-none float-end text-muted text-nowrap small">
<i class="bi bi-paperclip h6 me-1" v-if="message.Attachments"></i>
{{ getRelativeCreated(message) }}
</div>
<div class="text-truncate d-lg-none privacy">
<span v-if="message.From" :title="message.From.Address">{{ message.From.Name ?
message.From.Name : message.From.Address }}</span>
<span v-if="message.From" :title="message.From.Address">{{
message.From.Name ?
message.From.Name : message.From.Address
}}</span>
</div>
<div class="text-truncate d-none d-lg-block privacy">
<b v-if="message.From" :title="message.From.Address">{{ message.From.Name ?
message.From.Name : message.From.Address }}</b>
<b v-if="message.From" :title="message.From.Address">{{
message.From.Name ?
message.From.Name : message.From.Address
}}</b>
</div>
<div class="d-none d-lg-block text-truncate text-muted small privacy">
{{ getPrimaryEmailTo(message) }}
<span v-if="message.To && message.To.length > 1">
[+{{message.To.length - 1}}]
[+{{ message.To.length - 1 }}]
</span>
</div>
</div>
<div class="col-lg-6 mt-2 mt-lg-0">
<span class="badge text-bg-secondary me-1" v-for="t in message.Tags"
:title="'Filter messages tagged with ' + t" v-on:click="tagSearch($event, t)">
{{ t }}
</span>
<b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b>
</div>
<div class="d-none d-lg-block col-1 small text-end text-muted">
@@ -717,7 +777,8 @@ export default {
</div>
</div>
<Message v-if="message" :message="message"></Message>
<Message v-if="message" :message="message" :existingTags="existingTags" @load-messages="loadMessages">
</Message>
</div>
<div id="loading" v-if="loading">
<div class="d-flex justify-content-center align-items-center h-100">
@@ -740,7 +801,7 @@ export default {
This will permanently delete {{ formatNumber(total) }} message<span v-if="total > 1">s</span>.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" data-bs-dismiss="modal"
v-on:click="deleteAll">Delete</button>
</div>
@@ -761,8 +822,8 @@ export default {
This will mark {{ formatNumber(unread) }} message<span v-if="unread > 1">s</span> as read.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" data-bs-dismiss="modal"
v-on:click="markAllRead">Confirm</button>
</div>
</div>
@@ -787,8 +848,8 @@ export default {
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" data-bs-dismiss="modal"
v-on:click="requestNotifications">Enable notifications</button>
</div>
</div>
@@ -808,7 +869,7 @@ export default {
</div>
<div class="modal-body">
<a class="btn btn-warning d-block mb-3" v-if="appInfo.Version != appInfo.LatestVersion"
:href="'https://github.com/axllent/mailpit/releases/tag/'+appInfo.LatestVersion">
:href="'https://github.com/axllent/mailpit/releases/tag/' + appInfo.LatestVersion">
A new version of Mailpit ({{ appInfo.LatestVersion }}) is available.
</a>

View File

@@ -1,8 +1,7 @@
import { createApp } from 'vue';
import App from './App.vue';
import "./assets/bootstrap.scss";
import "./assets/styles.scss";
import "../../node_modules/bootstrap-icons/font/bootstrap-icons.scss";
import "bootstrap";
createApp(App).mount('#app')
createApp(App).mount('#app');

View File

@@ -1,2 +1,3 @@
$link-decoration: none;
$primary: #3465b5;
$primary: #2c3e50;
$list-group-disabled-color: #adb5bd;

View File

@@ -9,6 +9,7 @@
.navbar-brand {
color: #2d4a5d;
transition: all 0.2s;
img {
width: 40px;
@@ -24,6 +25,19 @@
}
}
.navbar-brand {
span {
opacity: 0.8;
transition: all 0.5s;
}
&:hover {
span {
opacity: 1;
}
}
}
#loading {
position: absolute;
top: 0;
@@ -71,16 +85,15 @@
min-height: 300px;
}
.list-group-item:first-child {
.list-group-item.message:first-child {
border-top: 0;
}
.message.selected {
background: $primary;
color: #fff;
background: $gray-300;
.text-muted {
color: #fff !important;
color: $body-color !important;
}
&.read {
@@ -136,6 +149,23 @@ body.blur {
}
}
// .tag.active {
// font-weight: bold;
// }
.form-select.tag-selector {
display: none;
}
.form-control.dropdown {
padding: 0;
border: 0;
input {
font-size: 0.875em;
}
}
/* PrismJS 1.29.0 - modified!
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css */
code[class*="language-"],

View File

@@ -1,5 +1,6 @@
import axios from 'axios';
import { Modal } from 'bootstrap';
import moment from 'moment';
// FakeModal is used to return a fake Bootstrap modal
@@ -12,7 +13,7 @@ FakeModal.prototype.show = function () { alert('open fake modal') }
const commonMixins = {
data() {
return {
loading: 0,
loading: 0
}
},
@@ -26,6 +27,10 @@ const commonMixins = {
return new Intl.NumberFormat().format(nr);
},
messageDate: function (d) {
return moment(d).format('ddd, D MMM YYYY, h:mm a');
},
// Ajax error message
handleError: function (error) {
// handle error
@@ -58,16 +63,6 @@ const commonMixins = {
return new FakeModal();
},
// generic modal get/set function
offcanvas: function (id) {
var e = document.getElementById(id);
if (e) {
return bootstrap.Offcanvas.getOrCreateInstance(e);
}
// in case there are open/close actions
return new FakeModal();
},
/**
* Axios GET request
*
@@ -211,4 +206,4 @@ const commonMixins = {
}
export default commonMixins
export default commonMixins;

View File

@@ -1,17 +1,19 @@
<script>
import commonMixins from '../mixins.js';
import moment from 'moment';
import Prism from "prismjs";
import Attachments from './Attachments.vue';
import MessageTags from './MessageTags.vue';
export default {
props: {
message: Object
message: Object,
existingTags: Array
},
components: {
Attachments
Attachments,
MessageTags
},
mixins: [commonMixins],
@@ -20,6 +22,7 @@ export default {
return {
srcURI: false,
iframes: [], // for resizing
tagComponent: false, // to force rerendering of component
}
},
@@ -27,42 +30,45 @@ export default {
message: {
handler(newQuestion) {
let self = this;
// delay 100ms to select first tab and add HTML highlighting (prev/next)
window.setTimeout(function() {
self.tagComponent = false;
// delay to select first tab and add HTML highlighting (prev/next)
self.$nextTick(function () {
self.renderUI();
}, 100)
self.tagComponent = true;
});
},
// force eager callback execution
immediate: true
}
},
mounted() {
let self = this;
self.tagComponent = false;
window.addEventListener("resize", self.resizeIframes);
self.renderUI();
var tabEl = document.getElementById('nav-raw-tab');
tabEl.addEventListener('shown.bs.tab', function (event) {
self.srcURI = 'api/v1/message/' + self.message.ID + '/raw';
});
self.tagComponent = true;
},
unmounted: function() {
unmounted: function () {
window.removeEventListener("resize", this.resizeIframes);
},
methods: {
renderUI: function() {
renderUI: function () {
let self = this;
// click the first non-disabled tab
document.querySelector('#nav-tab button:not([disabled])').click();
document.activeElement.blur(); // blur focus
document.getElementById('message-view').scrollTop = 0;
window.setTimeout(function(){
// delay 0.2s until vue has rendered the iframe content
window.setTimeout(function () {
let p = document.getElementById('preview-html');
if (p) {
// make links open in new window
let anchorEls = p.contentWindow.document.body.querySelectorAll('a');
@@ -83,13 +89,13 @@ export default {
window.Prism.manual = true;
Prism.highlightAll();
},
resizeIframe: function(el) {
resizeIframe: function (el) {
let i = el.target;
i.style.height = i.contentWindow.document.body.scrollHeight + 50 + 'px';
},
resizeIframes: function() {
resizeIframes: function () {
let h = document.getElementById('preview-html');
if (h) {
h.style.height = h.contentWindow.document.body.scrollHeight + 50 + 'px';
@@ -99,10 +105,6 @@ export default {
if (s) {
s.style.height = s.contentWindow.document.body.scrollHeight + 50 + 'px';
}
},
messageDate: function(d) {
return moment(d).format('ddd, D MMM YYYY, h:mm a');
}
}
}
@@ -129,52 +131,55 @@ export default {
<tr class="small">
<th>To</th>
<td class="privacy">
<span v-if="message.To" v-for="(t, i) in message.To">
<span v-if="message.To && message.To.length" v-for="(t, i) in message.To">
<template v-if="i > 0">, </template>
<span class="text-nowrap">{{ t.Name + " <" + t.Address +">" }}</span>
</span>
<span v-else>Undisclosed recipients</span>
<span class="text-nowrap">{{ t.Name + " <" + t.Address + ">" }}</span>
</span>
<span v-else>Undisclosed recipients</span>
</td>
</tr>
<tr v-if="message.Cc" class="small">
<th>CC</th>
<tr v-if="message.Cc && message.Cc.length" class="small">
<th>Cc</th>
<td class="privacy">
<span v-for="(t, i) in message.Cc">
<template v-if="i > 0">,</template>
{{ t.Name + " <" + t.Address +">" }}
</span>
{{ t.Name + " <" + t.Address + ">" }} </span>
</td>
</tr>
<tr v-if="message.Bcc" class="small">
<th>CC</th>
<tr v-if="message.Bcc && message.Bcc.length" class="small">
<th>Bcc</th>
<td class="privacy">
<span v-for="(t, i) in message.Bcc">
<template v-if="i > 0">,</template>
{{ t.Name + " <" + t.Address +">" }}
</span>
{{ t.Name + " <" + t.Address + ">" }} </span>
</td>
</tr>
<tr>
<th class="small">Subject</th>
<td><strong>{{ message.Subject }}</strong></td>
</tr>
<tr class="d-md-none">
<tr class="d-md-none small">
<th class="small">Date</th>
<td>{{ messageDate(message.Date) }}</td>
</tr>
<MessageTags :message="message" :existingTags="existingTags"
@load-messages="$emit('loadMessages')" v-if="tagComponent">
</MessageTags>
</tbody>
</table>
</div>
<div class="col-md-auto text-md-end mt-md-3">
<p class="text-muted small d-none d-md-block"><small>{{ messageDate(message.Date) }}</small></p>
<div class="dropdown mt-2" v-if="allAttachments(message)">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<!-- <p class="text-muted small d-none d-md-block mb-2"><small>{{ messageDate(message.Date) }}</small></p>
<p class="text-muted small d-none d-md-block"><small>Size: {{ getFileSize(message.Size) }}</small></p> -->
<div class="dropdown mt-2 mt-md-0" v-if="allAttachments(message)">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
Attachment<span v-if="allAttachments(message).length > 1">s</span>
({{ allAttachments(message).length }})
</button>
<ul class="dropdown-menu">
<li v-for="part in allAttachments(message)">
<a :href="'api/v1/message/'+message.ID+'/part/'+part.PartID" type="button"
<a :href="'api/v1/message/' + message.ID + '/part/' + part.PartID" type="button"
class="dropdown-item" target="_blank">
<i class="bi" :class="attachmentIcon(part)"></i>
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
@@ -188,41 +193,40 @@ export default {
<nav>
<div class="nav nav-tabs my-3" id="nav-tab" role="tablist">
<button class="nav-link" id="nav-html-tab" data-bs-toggle="tab"
data-bs-target="#nav-html" type="button" role="tab" aria-controls="nav-html"
aria-selected="true" v-if="message.HTML">HTML</button>
<button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab"
data-bs-target="#nav-html-source" type="button" role="tab" aria-controls="nav-html-source"
aria-selected="false" v-if="message.HTML">HTML Source</button>
<button class="nav-link" id="nav-plain-text-tab" data-bs-toggle="tab"
data-bs-target="#nav-plain-text" type="button" role="tab" aria-controls="nav-plain-text"
aria-selected="false" :class="message.HTML == '' ? 'show':''">Text</button>
<button class="nav-link" id="nav-raw-tab" data-bs-toggle="tab"
data-bs-target="#nav-raw" type="button" role="tab" aria-controls="nav-raw"
aria-selected="false">Raw</button>
<button class="nav-link" id="nav-html-tab" data-bs-toggle="tab" data-bs-target="#nav-html" type="button"
role="tab" aria-controls="nav-html" aria-selected="true" v-if="message.HTML">HTML</button>
<button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab" data-bs-target="#nav-html-source"
type="button" role="tab" aria-controls="nav-html-source" aria-selected="false"
v-if="message.HTML">HTML Source</button>
<button class="nav-link" id="nav-plain-text-tab" data-bs-toggle="tab" data-bs-target="#nav-plain-text"
type="button" role="tab" aria-controls="nav-plain-text" aria-selected="false"
:class="message.HTML == '' ? 'show' : ''">Text</button>
<button class="nav-link" id="nav-raw-tab" data-bs-toggle="tab" data-bs-target="#nav-raw" type="button"
role="tab" aria-controls="nav-raw" aria-selected="false">Raw</button>
</div>
</nav>
<div class="tab-content mb-5" id="nav-tabContent">
<div v-if="message.HTML != ''" class="tab-pane fade show" id="nav-html" role="tabpanel"
aria-labelledby="nav-html-tab" tabindex="0">
<iframe target-blank="" class="tab-pane" id="preview-html" :srcdoc="message.HTML" v-on:load="resizeIframe"
seamless frameborder="0" style="width: 100%; height: 100%;">
<iframe target-blank="" class="tab-pane" id="preview-html" :srcdoc="message.HTML"
v-on:load="resizeIframe" seamless frameborder="0" style="width: 100%; height: 100%;">
</iframe>
<Attachments v-if="allAttachments(message).length" :message="message" :attachments="allAttachments(message)"></Attachments>
<Attachments v-if="allAttachments(message).length" :message="message"
:attachments="allAttachments(message)"></Attachments>
</div>
<div class="tab-pane fade" id="nav-html-source" role="tabpanel"
aria-labelledby="nav-html-source-tab" tabindex="0" v-if="message.HTML">
<div class="tab-pane fade" id="nav-html-source" role="tabpanel" aria-labelledby="nav-html-source-tab"
tabindex="0" v-if="message.HTML">
<pre><code class="language-html">{{ message.HTML }}</code></pre>
</div>
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel"
aria-labelledby="nav-plain-text-tab" tabindex="0" :class="message.HTML == '' ? 'show':''">
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel" aria-labelledby="nav-plain-text-tab"
tabindex="0" :class="message.HTML == '' ? 'show' : ''">
<div class="text-view">{{ message.Text }}</div>
<Attachments v-if="allAttachments(message).length" :message="message" :attachments="allAttachments(message)"></Attachments>
<Attachments v-if="allAttachments(message).length" :message="message"
:attachments="allAttachments(message)"></Attachments>
</div>
<div class="tab-pane fade" id="nav-raw" role="tabpanel" aria-labelledby="nav-raw-tab"
tabindex="0">
<iframe v-if="srcURI" :src="srcURI" v-on:load="resizeIframe"
seamless frameborder="0" style="width: 100%; height: 300px;" id="message-src"></iframe>
<div class="tab-pane fade" id="nav-raw" role="tabpanel" aria-labelledby="nav-raw-tab" tabindex="0">
<iframe v-if="srcURI" :src="srcURI" v-on:load="resizeIframe" seamless frameborder="0"
style="width: 100%; height: 300px;" id="message-src"></iframe>
</div>
</div>
</div>

View File

@@ -0,0 +1,28 @@
<script>
import commonMixins from '../mixins.js';
export default {
props: {
message: Object
},
mixins: [commonMixins]
}
</script>
<template>
<div class="card mt-4">
<div class="card-body text-muted small">
<p class="card-text">
<b>Message date:</b><br>
<small>{{ messageDate(message.Date) }}</small>
</p>
<p class="card-text">
<b>Size:</b> {{ getFileSize(message.Size) }}
</p>
<p class="card-text" v-if="allAttachments(message).length">
<b>Attachments:</b> {{ allAttachments(message).length }}
</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,72 @@
<script>
import commonMixins from '../mixins.js';
import Tags from "bootstrap5-tags";
export default {
props: {
message: Object,
existingTags: Array
},
mixins: [commonMixins],
data() {
return {
messageTags: [],
}
},
mounted() {
let self = this;
self.loaded = false;
self.messageTags = self.message.Tags;
// delay until vue has rendered
self.$nextTick(function () {
Tags.init("select[multiple]");
self.$nextTick(function () {
self.loaded = true;
});
});
},
watch: {
messageTags() {
if (this.loaded) {
this.saveTags();
}
}
},
methods: {
saveTags: function () {
let self = this;
var data = {
ids: [this.message.ID],
tags: this.messageTags
}
self.put('api/v1/tags', data, function (response) {
self.scrollInPlace = true;
self.$emit('loadMessages');
});
}
}
}
</script>
<template>
<tr class="small">
<th>Tags</th>
<td>
<select class="form-select small tag-selector" v-model="messageTags" multiple data-allow-new="true"
data-clear-end="true" data-allow-clear="true" data-placeholder="Add tags..."
data-badge-style="secondary" data-regex="^([a-zA-Z0-9\-\ \_]){3,}$" data-separator="|,|">
<option value="">Type a tag...</option><!-- you need at least one option with the placeholder -->
<option v-for="t in existingTags" :value="t">{{ t }}</option>
</select>
<div class="invalid-feedback">Please select a valid tag.</div>
</td>
</tr>
</template>

22
server/ui/favicon.svg Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="500"
height="460"
viewBox="0 0 132.292 121.708"
version="1.1"
id="svg6"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs10" />
<path
d="M12.321 0l53.861 53.918L120.365 0zM5.155 9.025l60.842 59.673 61.211-59.489-.185 36.835L66.921 70.54l15.164 12.616-8.137 5.986-41.609.184c-4.838-.022-25.877-18.34-27.185-41.255z"
fill-opacity=".941"
fill="#2d4a5f"
id="path2"
style="fill:#415066;fill-opacity:1" />
<path
d="M78.385 72.049l53.907-21.679-8.031 57.318-11.845-9.132c-21.727 23.171-45.255 26.289-67.997 20.837S12.281 98.39 5.155 83.8-.67 53.116 2.843 38.769c1.13 10.511-1.313 16.316 6.38 33.612 6.31 11.399 14.413 20.417 25.89 24.956 13.9 6.195 32.247 3.357 41.701-3.039l14.24-12.156z"
fill="#00b786"
id="path4" />
</svg>

After

Width:  |  Height:  |  Size: 898 B

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noindex, nofollow, noarchive">
<link rel="icon" href="mailpit.svg">
<link rel="icon" href="favicon.svg">
<title>Mailpit</title>
<link rel=stylesheet href="dist/app.css">
</head>

View File

@@ -1 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="460" viewBox="0 0 132.292 121.708" xmlns:v="https://vecta.io/nano"><path d="M12.321 0l53.861 53.918L120.365 0zM5.155 9.025l60.842 59.673 61.211-59.489-.185 36.835L66.921 70.54l15.164 12.616-8.137 5.986-41.609.184c-4.838-.022-25.877-18.34-27.185-41.255z" fill-opacity=".941" fill="#2d4a5f"/><path d="M78.385 72.049l53.907-21.679-8.031 57.318-11.845-9.132c-21.727 23.171-45.255 26.289-67.997 20.837S12.281 98.39 5.155 83.8-.67 53.116 2.843 38.769c1.13 10.511-1.313 16.316 6.38 33.612 6.31 11.399 14.413 20.417 25.89 24.956 13.9 6.195 32.247 3.357 41.701-3.039l14.24-12.156z" fill="#00b786"/></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="500"
height="460"
viewBox="0 0 132.292 121.708"
version="1.1"
id="svg6"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs10" />
<path
d="M12.321 0l53.861 53.918L120.365 0zM5.155 9.025l60.842 59.673 61.211-59.489-.185 36.835L66.921 70.54l15.164 12.616-8.137 5.986-41.609.184c-4.838-.022-25.877-18.34-27.185-41.255z"
fill-opacity=".941"
fill="#2d4a5f"
id="path2"
style="fill:#ffffff;fill-opacity:1" />
<path
d="M78.385 72.049l53.907-21.679-8.031 57.318-11.845-9.132c-21.727 23.171-45.255 26.289-67.997 20.837S12.281 98.39 5.155 83.8-.67 53.116 2.843 38.769c1.13 10.511-1.313 16.316 6.38 33.612 6.31 11.399 14.413 20.417 25.89 24.956 13.9 6.195 32.247 3.357 41.701-3.039l14.24-12.156z"
fill="#00b786"
id="path4" />
</svg>

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 898 B

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -9,7 +9,7 @@ import (
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/utils/logger"
"github.com/gorilla/websocket"
)

View File

@@ -7,7 +7,7 @@ package websockets
import (
"encoding/json"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/utils/logger"
)
// Hub maintains the set of active clients and broadcasts messages to the

View File

@@ -1,81 +0,0 @@
package smtpd
import (
"bytes"
"net"
"net/mail"
"regexp"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/storage"
"github.com/mhale/smtpd"
)
func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
msg, err := mail.ReadMessage(bytes.NewReader(data))
if err != nil {
logger.Log().Errorf("error parsing message: %s", err.Error())
return err
}
if _, err := storage.Store(data); err != nil {
// Value with size 4800709 exceeded 1048576 limit
re := regexp.MustCompile(`(Value with size \d+ exceeded \d+ limit)`)
tooLarge := re.FindStringSubmatch(err.Error())
if len(tooLarge) > 0 {
logger.Log().Errorf("[db] error storing message: %s", tooLarge[0])
} else {
logger.Log().Errorf("[db] error storing message")
logger.Log().Errorf(err.Error())
}
return err
}
subject := msg.Header.Get("Subject")
logger.Log().Debugf("[smtp] received mail from %s for %s with subject %s", from, to[0], subject)
return nil
}
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
return config.SMTPAuth.Match(string(username), string(password)), nil
}
// Listen starts the SMTPD server
func Listen() error {
if config.SMTPSSLCert != "" {
logger.Log().Info("[smtp] enabling TLS")
}
if config.SMTPAuthFile != "" {
logger.Log().Info("[smtp] enabling authentication")
}
logger.Log().Infof("[smtp] starting on %s", config.SMTPListen)
return listenAndServe(config.SMTPListen, mailHandler, authHandler)
}
func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHandler) error {
srv := &smtpd.Server{
Addr: addr,
Handler: handler,
Appname: "Mailpit",
Hostname: "",
AuthHandler: nil,
AuthRequired: false,
}
if config.SMTPAuthFile != "" {
srv.AuthHandler = authHandler
srv.AuthRequired = true
}
if config.SMTPSSLCert != "" {
err := srv.ConfigureTLS(config.SMTPSSLCert, config.SMTPSSLKey)
if err != nil {
return err
}
}
return srv.ListenAndServe()
}

View File

@@ -14,14 +14,15 @@ import (
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"syscall"
"time"
"github.com/GuiaBolso/darwin"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/server/websockets"
"github.com/axllent/mailpit/utils/logger"
"github.com/jhillyerd/enmime"
"github.com/klauspost/compress/zstd"
"github.com/leporo/sqlf"
@@ -65,6 +66,12 @@ var (
);
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);`,
},
}
)
@@ -174,7 +181,7 @@ func Store(body []byte) (string, error) {
return "", nil
}
var from *mail.Address
from := &mail.Address{}
fromJSON := addressToSlice(env, "From")
if len(fromJSON) > 0 {
from = fromJSON[0]
@@ -194,13 +201,30 @@ func Store(body []byte) (string, error) {
Attachments: len(env.Attachments),
}
// use message date instead of created date
if config.UseMessageDates {
if mDate, err := env.Date(); err == nil {
obj.Created = mDate
}
}
// generate the search text
searchText := createSearchText(env)
// generate unique ID
id := uuid.NewV4().String()
b, err := json.Marshal(obj)
summaryJSON, err := json.Marshal(obj)
if err != nil {
return "", err
}
tagData := findTags(&body)
tagJSON, err := json.Marshal(tagData)
if err != nil {
return "", err
}
// begin a transaction to ensure both the message
// and data are stored successfully
@@ -213,8 +237,8 @@ func Store(body []byte) (string, error) {
// roll back if it fails
defer tx.Rollback()
// insert summary
_, err = tx.Exec("INSERT INTO mailbox(ID, Data, Search, Read) values(?,?,?, 0)", id, string(b), searchText)
// insert mail summary data
_, err = tx.Exec("INSERT INTO mailbox(ID, Data, Search, Tags, Read) values(?,?,?,?,0)", id, string(summaryJSON), searchText, string(tagJSON))
if err != nil {
return "", err
}
@@ -230,12 +254,13 @@ func Store(body []byte) (string, error) {
return "", err
}
// return summary
c := &Summary{}
if err := json.Unmarshal(b, c); err != nil {
c := &MessageSummary{}
if err := json.Unmarshal(summaryJSON, c); err != nil {
return "", err
}
c.Tags = tagData
c.ID = id
websockets.Broadcast("new", c)
@@ -247,11 +272,11 @@ func Store(body []byte) (string, error) {
// List returns a subset of messages from the mailbox,
// sorted latest to oldest
func List(start, limit int) ([]Summary, error) {
results := []Summary{}
func List(start, limit int) ([]MessageSummary, error) {
results := []MessageSummary{}
q := sqlf.From("mailbox").
Select(`ID, Data, Read`).
Select(`ID, Data, Tags, Read`).
OrderBy("Sort DESC").
Limit(limit).
Offset(start)
@@ -259,16 +284,21 @@ func List(start, limit int) ([]Summary, error) {
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
var id string
var summary string
var tags string
var read int
em := Summary{}
em := MessageSummary{}
if err := row.Scan(&id, &summary, &read); err != nil {
if err := row.Scan(&id, &summary, &tags, &read); err != nil {
logger.Log().Error(err)
return
}
err := json.Unmarshal([]byte(summary), &em)
if err != nil {
if err := json.Unmarshal([]byte(summary), &em); err != nil {
logger.Log().Error(err)
return
}
if err := json.Unmarshal([]byte(tags), &em.Tags); err != nil {
logger.Log().Error(err)
return
}
@@ -291,8 +321,8 @@ func List(start, limit int) ([]Summary, error) {
// The search is broken up by segments (exact phrases can be quoted), and interprits specific terms such as:
// 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 Search(search string, start, limit int) ([]Summary, error) {
results := []Summary{}
func Search(search string, start, limit int) ([]MessageSummary, error) {
results := []MessageSummary{}
tsStart := time.Now()
s := strings.ToLower(search)
@@ -314,17 +344,22 @@ func Search(search string, start, limit int) ([]Summary, error) {
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
var id string
var summary string
var tags string
var read int
var ignore string
em := Summary{}
em := MessageSummary{}
if err := row.Scan(&id, &summary, &read, &ignore, &ignore, &ignore, &ignore); err != nil {
if err := row.Scan(&id, &summary, &tags, &read, &ignore, &ignore, &ignore, &ignore, &ignore, &ignore); err != nil {
logger.Log().Error(err)
return
}
err := json.Unmarshal([]byte(summary), &em)
if err != nil {
if err := json.Unmarshal([]byte(summary), &em); err != nil {
logger.Log().Error(err)
return
}
if err := json.Unmarshal([]byte(tags), &em.Tags); err != nil {
logger.Log().Error(err)
return
}
@@ -346,7 +381,8 @@ func Search(search string, start, limit int) ([]Summary, error) {
return results, err
}
// GetMessage returns a data.Message generated from the mailbox_data collection.
// GetMessage returns a Message generated from the mailbox_data collection.
// If the message lacks a date header, then the received datetime is used.
func GetMessage(id string) (*Message, error) {
raw, err := GetMessageRaw(id)
if err != nil {
@@ -368,7 +404,35 @@ func GetMessage(id string) (*Message, error) {
from = &mail.Address{Name: env.GetHeader("From")}
}
date, _ := env.Date()
date, err := env.Date()
if err != nil {
// return received datetime when message does not contain a date header
q := sqlf.From("mailbox").
Select(`Data`).
OrderBy("Sort DESC").
Where(`ID = ?`, id)
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
var summary string
em := MessageSummary{}
if err := row.Scan(&summary); err != nil {
logger.Log().Error(err)
return
}
if err := json.Unmarshal([]byte(summary), &em); err != nil {
logger.Log().Error(err)
return
}
logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id)
date = em.Created
}); err != nil {
logger.Log().Error(err)
}
}
obj := Message{
ID: id,
@@ -379,6 +443,7 @@ func GetMessage(id string) (*Message, error) {
Cc: addressToSlice(env, "Cc"),
Bcc: addressToSlice(env, "Bcc"),
Subject: env.GetHeader("Subject"),
Tags: getMessageTags(id),
Size: len(raw),
Text: env.Text,
}
@@ -387,6 +452,8 @@ func GetMessage(id string) (*Message, error) {
var re = regexp.MustCompile(`(?U)<base .*>`)
html := re.ReplaceAllString(env.HTML, "")
obj.HTML = html
obj.Inline = []Attachment{}
obj.Attachments = []Attachment{}
for _, i := range env.Inlines {
if i.FileName != "" || i.ContentID != "" {
@@ -643,6 +710,8 @@ func DeleteAllMessages() error {
dbLastAction = time.Now()
dbDataDeleted = false
websockets.Broadcast("prune", nil)
return err
}
@@ -655,9 +724,42 @@ func StatsGet() MailboxStats {
dbLastAction = time.Now()
q := sqlf.From("mailbox").
Select(`DISTINCT Tags`).
Where("Tags != ?", "[]")
var tags = []string{}
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
var tagData string
t := []string{}
if err := row.Scan(&tagData); err != nil {
logger.Log().Error(err)
return
}
if err := json.Unmarshal([]byte(tagData), &t); err != nil {
logger.Log().Error(err)
return
}
for _, tag := range t {
if !inArray(tag, tags) {
tags = append(tags, tag)
}
}
}); err != nil {
logger.Log().Error(err)
}
sort.Strings(tags)
return MailboxStats{
Total: total,
Unread: unread,
Tags: tags,
}
}

View File

@@ -14,10 +14,12 @@ func searchParser(args []string, start, limit int) *sqlf.Stmt {
}
q := sqlf.From("mailbox").
Select(`ID, Data, read,
json_extract(Data, '$.To') as ToJSON,
json_extract(Data, '$.From') as FromJSON,
json_extract(Data, '$.Subject') as Subject,
Select(`ID, Data, Tags, Read,
json_extract(Data, '$.To') as ToJSON,
json_extract(Data, '$.From') as FromJSON,
IFNULL(json_extract(Data, '$.Cc'), '{}') as CcJSON,
IFNULL(json_extract(Data, '$.Bcc'), '{}') as BccJSON,
json_extract(Data, '$.Subject') as Subject,
json_extract(Data, '$.Attachments') as Attachments
`).
OrderBy("Sort DESC").
@@ -63,6 +65,24 @@ func searchParser(args []string, start, limit int) *sqlf.Stmt {
q.Where("FromJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "cc:") {
w = cleanString(w[3:])
if w != "" {
if exclude {
q.Where("CcJSON NOT LIKE ?", "%"+escPercentChar(w)+"%")
} else {
q.Where("CcJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "bcc:") {
w = cleanString(w[4:])
if w != "" {
if exclude {
q.Where("BccJSON NOT LIKE ?", "%"+escPercentChar(w)+"%")
} else {
q.Where("BccJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "subject:") {
w = cleanString(w[8:])
if w != "" {
@@ -72,6 +92,15 @@ func searchParser(args []string, start, limit int) *sqlf.Stmt {
q.Where("Subject LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "tag:") {
w = cleanString(w[4:])
if w != "" {
if exclude {
q.Where("Tags NOT LIKE ?", "%\""+escPercentChar(w)+"\"%")
} else {
q.Where("Tags LIKE ?", "%\""+escPercentChar(w)+"\"%")
}
}
} else if w == "is:read" {
if exclude {
q.Where("Read = 0")

View File

@@ -17,6 +17,7 @@ type Message struct {
Bcc []*mail.Address
Subject string
Date time.Time
Tags []string
Text string
HTML string
Size int
@@ -33,8 +34,8 @@ type Attachment struct {
Size int
}
// Summary struct for frontend messages
type Summary struct {
// MessageSummary struct for frontend messages
type MessageSummary struct {
ID string
Read bool
From *mail.Address
@@ -43,6 +44,7 @@ type Summary struct {
Bcc []*mail.Address
Subject string
Created time.Time
Tags []string
Size int
Attachments int
}
@@ -51,6 +53,7 @@ type Summary struct {
type MailboxStats struct {
Total int
Unread int
Tags []string
}
// AttachmentSummary returns a summary of the attachment without any binary data

86
storage/tags.go Normal file
View File

@@ -0,0 +1,86 @@
package storage
import (
"context"
"encoding/json"
"regexp"
"sort"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
"github.com/leporo/sqlf"
)
// SetTags will set the tags for a given message ID, used via API
func SetTags(id string, tags []string) error {
applyTags := []string{}
reg := regexp.MustCompile(`\s+`)
for _, t := range tags {
t = strings.TrimSpace(reg.ReplaceAllString(t, " "))
if t != "" && config.TagRegexp.MatchString(t) && !inArray(t, applyTags) {
applyTags = append(applyTags, t)
}
}
tagJSON, err := json.Marshal(applyTags)
if err != nil {
logger.Log().Errorf("[db] setting tags for message %s", id)
return err
}
_, err = sqlf.Update("mailbox").
Set("Tags", string(tagJSON)).
Where("ID = ?", id).
ExecAndClose(context.Background(), db)
if err == nil {
logger.Log().Debugf("[db] set tags %s for message %s", string(tagJSON), id)
}
return err
}
// Used to auto-apply tags to new messages
func findTags(message *[]byte) []string {
tags := []string{}
if len(config.SMTPTags) == 0 {
return tags
}
str := strings.ToLower(string(*message))
for _, t := range config.SMTPTags {
if !inArray(t.Tag, tags) && strings.Contains(str, t.Match) {
tags = append(tags, t.Tag)
}
}
sort.Strings(tags)
return tags
}
// Get message tags from the database for a given message ID.
// Used when parsing a raw email.
func getMessageTags(id string) []string {
tags := []string{}
var data string
q := sqlf.From("mailbox").
Select(`Tags`).To(&data).
Where(`ID = ?`, id)
err := q.QueryRowAndClose(context.Background(), db)
if err != nil {
logger.Log().Error(err)
return tags
}
if err := json.Unmarshal([]byte(data), &tags); err != nil {
logger.Log().Error(err)
return tags
}
return tags
}

View File

@@ -10,8 +10,8 @@ import (
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/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"
@@ -19,7 +19,10 @@ import (
// Return a header field as a []*mail.Address, or "null" is not found/empty
func addressToSlice(env *enmime.Envelope, key string) []*mail.Address {
data, _ := env.AddressList(key)
data, err := env.AddressList(key)
if err != nil || data == nil {
return []*mail.Address{}
}
return data
}
@@ -34,7 +37,12 @@ func createSearchText(env *enmime.Envelope) string {
b.WriteString(env.GetHeader("To") + " ")
b.WriteString(env.GetHeader("Cc") + " ")
b.WriteString(env.GetHeader("Bcc") + " ")
h := strings.TrimSpace(html2text.HTML2Text(env.HTML))
h := strings.TrimSpace(
html2text.HTML2TextWithOptions(
env.HTML,
html2text.WithLinksInnerText(),
),
)
if h != "" {
b.WriteString(h + " ")
} else {
@@ -53,7 +61,7 @@ func createSearchText(env *enmime.Envelope) string {
// CleanString removes unwanted characters from stored search text and search queries
func cleanString(str string) string {
// remove/replace new lines
re := regexp.MustCompile(`(\r?\n|\t|>|<|"|:|\,|;)`)
re := regexp.MustCompile(`(\r?\n|\t|>|<|"|\,|;)`)
str = re.ReplaceAllString(str, " ")
// remove duplicate whitespace and trim
@@ -159,6 +167,17 @@ func isFile(path string) bool {
return true
}
func inArray(k string, arr []string) bool {
k = strings.ToLower(k)
for _, v := range arr {
if strings.ToLower(v) == k {
return true
}
}
return false
}
// escPercentChar replaces `%` with `%%` for SQL searches
func escPercentChar(s string) string {
return strings.ReplaceAll(s, "%", "%%")

View File

@@ -13,7 +13,7 @@ import (
"path/filepath"
"runtime"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/utils/logger"
"github.com/axllent/semver"
)