Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e843de6166 | ||
|
|
b6f2618b34 | ||
|
|
31c0a501e8 | ||
|
|
08288e904d | ||
|
|
dfb455c59c | ||
|
|
5e00013a8d | ||
|
|
c5a8836b7e | ||
|
|
ae73c721db | ||
|
|
9ae9104ca3 | ||
|
|
aa2dc4cf62 | ||
|
|
cffbd3f884 | ||
|
|
a05cc59800 | ||
|
|
924ad9b064 | ||
|
|
b63e9b465b | ||
|
|
124f1c2bde | ||
|
|
64461c17a1 | ||
|
|
0ff6b18b43 | ||
|
|
1a638cf8ea | ||
|
|
126fa66d58 | ||
|
|
1f95461651 | ||
|
|
176f128057 | ||
|
|
031b5697e4 | ||
|
|
19f51c8931 | ||
|
|
7c62dca14b | ||
|
|
584d94b8e7 | ||
|
|
23370eab0f | ||
|
|
4f5b5e2f02 | ||
|
|
def9602811 | ||
|
|
3d63a27458 | ||
|
|
389f248603 | ||
|
|
04462f76c6 | ||
|
|
2752a09ca7 | ||
|
|
8eed8d92e5 | ||
|
|
6a82dd0eb2 | ||
|
|
b5b0c173c3 | ||
|
|
9c8329a05c | ||
|
|
7c329b56f8 | ||
|
|
26a84bc257 | ||
|
|
d65de12714 | ||
|
|
5ed55e58e1 | ||
|
|
84d3384120 | ||
|
|
efc9c10f83 | ||
|
|
962af81653 | ||
|
|
7deddc3119 | ||
|
|
058bc31e28 | ||
|
|
8e84b96233 | ||
|
|
a8dddbaa7b | ||
|
|
8f9876a0a3 | ||
|
|
17ecdb6165 | ||
|
|
eba934c0e0 | ||
|
|
31885008ed | ||
|
|
c48da61097 | ||
|
|
c532870adc | ||
|
|
85291683b6 | ||
|
|
09399db612 | ||
|
|
ea753f6948 | ||
|
|
0f73f7d261 | ||
|
|
e188325ddd | ||
|
|
6ab6d5fa2d | ||
|
|
f6545b55a4 | ||
|
|
1b798c5514 | ||
|
|
f16b105d26 | ||
|
|
af7df617af | ||
|
|
4e6d8e5803 | ||
|
|
14d2715832 | ||
|
|
6d902293c1 | ||
|
|
b423c26537 | ||
|
|
75db0e2911 | ||
|
|
0f21f2e4b5 | ||
|
|
c4a695e627 | ||
|
|
62cf75f8fb | ||
|
|
5350e2eb08 | ||
|
|
3bb9f4162a | ||
|
|
2d07683a28 | ||
|
|
fc753677f6 | ||
|
|
ab0c91545a | ||
|
|
b6e1b68c90 | ||
|
|
182d32a2c8 | ||
|
|
169c476c56 | ||
|
|
57b0e1666f | ||
|
|
a9ce35b741 | ||
|
|
fb03fda9ea | ||
|
|
e2254a68ef | ||
|
|
755ff37cdc | ||
|
|
03f30b01bf | ||
|
|
27d49417d7 | ||
|
|
aeeb732681 | ||
|
|
73a92a3952 | ||
|
|
9cd81afe7c | ||
|
|
41270b956e | ||
|
|
dfad730b21 | ||
|
|
3d31ae7da4 | ||
|
|
f0723fb64a | ||
|
|
b905ba4ec5 | ||
|
|
7675cd162f | ||
|
|
dff5a605b4 | ||
|
|
3f3b8a6d97 | ||
|
|
fc595c031d | ||
|
|
a897004dc1 | ||
|
|
6917477533 | ||
|
|
eede2bff99 | ||
|
|
de0549e60a | ||
|
|
17caa21afd | ||
|
|
4656717046 | ||
|
|
72fdbb8364 | ||
|
|
37b4f1f566 | ||
|
|
464fbf818c | ||
|
|
6360a69ff6 | ||
|
|
054438b952 | ||
|
|
cb6085790b | ||
|
|
1bd0c6ac74 | ||
|
|
7cb46ba869 | ||
|
|
6efe99ffdf | ||
|
|
cc121e4b27 | ||
|
|
ee86260651 | ||
|
|
cab9f8a729 | ||
|
|
790fbe69fd | ||
|
|
51074a9d72 | ||
|
|
28b4f2d09d | ||
|
|
b6c1c180c9 | ||
|
|
264ad1bf9f | ||
|
|
7d63c75557 | ||
|
|
0c4c2881c8 | ||
|
|
56999e97e2 | ||
|
|
d238675011 | ||
|
|
fea3b0a422 | ||
|
|
24b1dfa040 | ||
|
|
ab73a4bcfb | ||
|
|
df3b27b5e0 | ||
|
|
52bf19a40c | ||
|
|
c1694f1a22 | ||
|
|
894da47eda | ||
|
|
1718ec00e5 | ||
|
|
70df34d071 | ||
|
|
d101ec045d | ||
|
|
a1d8840da2 | ||
|
|
ed1bb83bda | ||
|
|
4b2e8b0174 | ||
|
|
594c4817a4 | ||
|
|
47a556d05e | ||
|
|
e3e7c09e81 | ||
|
|
98a932ecdb | ||
|
|
d47eb09c54 | ||
|
|
acee53537c | ||
|
|
b18bcebd51 | ||
|
|
0502056678 | ||
|
|
6901a20661 | ||
|
|
10752a58c8 | ||
|
|
c8bf742c18 | ||
|
|
7313862ad5 | ||
|
|
8976124b3d | ||
|
|
4fbff688ec | ||
|
|
dca70a50c3 | ||
|
|
eb50304a13 | ||
|
|
858dfca321 | ||
|
|
5e09dec667 | ||
|
|
638ea3efa8 | ||
|
|
06bfc3b6e3 | ||
|
|
c2d34f3071 | ||
|
|
be582291c7 | ||
|
|
646fe072be | ||
|
|
deba47f6d1 | ||
|
|
5f9efebeb3 | ||
|
|
06aa7a2dea | ||
|
|
2c3c436fc1 | ||
|
|
6f2dd83936 | ||
|
|
b850c89ae0 | ||
|
|
cc327ab3ba | ||
|
|
1886d78001 | ||
|
|
63cbafa182 | ||
|
|
95dacfc5db | ||
|
|
067d218f4b | ||
|
|
3dd004ea4b | ||
|
|
6570217bfd | ||
|
|
54635b748a | ||
|
|
0ea4cab33b | ||
|
|
0fde942e0d | ||
|
|
b09d7ac75d | ||
|
|
fc2fdd20f6 | ||
|
|
cbbac40c0d | ||
|
|
6bc02fd4d4 | ||
|
|
57cfb2611c | ||
|
|
ba24d145ff | ||
|
|
376e799eb0 | ||
|
|
1dfadda07e | ||
|
|
fc0a7358ab | ||
|
|
d229b34d98 | ||
|
|
cbc3fe59a8 | ||
|
|
ab771cf76c | ||
|
|
7a27e09d23 | ||
|
|
cdce989a9c | ||
|
|
61dd3eddc5 | ||
|
|
290e48d875 | ||
|
|
e7ea94a5d2 | ||
|
|
43bd2a18ea | ||
|
|
ec95e58e13 | ||
|
|
70ac9c73ea | ||
|
|
0fcdcdd5f6 | ||
|
|
ea12a1ee56 | ||
|
|
9345ed60c6 | ||
|
|
0a13cf8304 | ||
|
|
4ebbdab7c0 | ||
|
|
cea9518b4b | ||
|
|
a9220277d6 | ||
|
|
bd45d9dffe | ||
|
|
baaf3a3a23 | ||
|
|
2e95a75d32 | ||
|
|
53d2296ff5 | ||
|
|
e8bf803ca0 | ||
|
|
d9dc000e89 | ||
|
|
205611856b | ||
|
|
5d396b9f25 | ||
|
|
4b95c6bda0 | ||
|
|
9982948c81 | ||
|
|
614b63cf28 | ||
|
|
b1027ca844 | ||
|
|
2176ad6ca2 | ||
|
|
971753e576 |
@@ -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
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [axllent]
|
||||
23
.github/dependabot.yml
vendored
Normal 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"
|
||||
23
.github/workflows/build-docker.yml
vendored
@@ -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
|
||||
|
||||
@@ -26,12 +22,23 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
|
||||
- name: Parse semver
|
||||
id: semver_parser
|
||||
uses: booxmedialtd/ws-action-parse-semver@v1.4.7
|
||||
with:
|
||||
input_string: '${{ github.ref_name }}'
|
||||
version_extractor_regex: 'v(.*)$'
|
||||
|
||||
- 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 }}
|
||||
axllent/mailpit:v${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}
|
||||
|
||||
23
.github/workflows/close-stale-issues.yml
vendored
Normal 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@v8.0.0
|
||||
with:
|
||||
days-before-issue-stale: 21
|
||||
days-before-issue-close: 7
|
||||
exempt-issue-labels: "enhancement,bug,javascript,docker"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 21 days with no activity."
|
||||
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 }}
|
||||
12
.github/workflows/release-build.yml
vendored
@@ -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.37
|
||||
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 }}"
|
||||
|
||||
4
.github/workflows/tests.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- uses: actions/checkout@v3
|
||||
@@ -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
|
||||
|
||||
326
CHANGELOG.md
@@ -2,7 +2,277 @@
|
||||
|
||||
Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## 1.2.3
|
||||
## [v1.6.5]
|
||||
|
||||
### Feature
|
||||
- Add Access-Control-Allow-Methods methods when CORS origin is set
|
||||
|
||||
|
||||
## [v1.6.4]
|
||||
|
||||
### Bugfix
|
||||
- Fix UI images not displaying when multiple cid names overlap
|
||||
|
||||
|
||||
## [v1.6.3]
|
||||
|
||||
### Feature
|
||||
- Display clickable toast notifications for new messages
|
||||
|
||||
|
||||
## [v1.6.2]
|
||||
|
||||
### Bugfix
|
||||
- If set use return-path address as SMTP from address
|
||||
|
||||
|
||||
## [v1.6.1]
|
||||
|
||||
### Bugfix
|
||||
- Add API release route again (bad merge)
|
||||
|
||||
|
||||
## [v1.6.0]
|
||||
|
||||
### API
|
||||
- Enable cross-origin resource sharing (CORS) configuration
|
||||
- Message relay / release
|
||||
- Include Return-Path in message summary data
|
||||
|
||||
### Feature
|
||||
- Inject/update Bcc header for missing addresses when SMTP recipients do not match messsage headers
|
||||
|
||||
### Libs
|
||||
- Update Go modules
|
||||
- Update node modules
|
||||
|
||||
### UI
|
||||
- Display Return-Path if different to the From address
|
||||
- Message release functionality
|
||||
|
||||
|
||||
## [v1.5.5]
|
||||
|
||||
### Docker
|
||||
- Add Docker image tag for major/minor version
|
||||
|
||||
### Feature
|
||||
- Update listen regex to allow IPv6 addresses ([#85](https://github.com/axllent/mailpit/issues/85))
|
||||
|
||||
|
||||
## [v1.5.4]
|
||||
|
||||
### Feature
|
||||
- Mobile and tablet HTML preview toggle in desktop mode
|
||||
|
||||
|
||||
## [v1.5.3]
|
||||
|
||||
### Bugfix
|
||||
- Enable SMTP auth flags to be set via env
|
||||
|
||||
|
||||
## [v1.5.2]
|
||||
|
||||
### API
|
||||
- Include Reply-To in message summary (including Web UI)
|
||||
|
||||
### UI
|
||||
- Tab to view formatted message headers
|
||||
|
||||
|
||||
## [v1.5.1]
|
||||
|
||||
### Feature
|
||||
- Add 'o', 'b' & 's' ignored flags for sendmail
|
||||
|
||||
### Libs
|
||||
- Update Go modules
|
||||
- Update node modules
|
||||
|
||||
|
||||
## [v1.5.0]
|
||||
|
||||
### API
|
||||
- Return received datetime when message does not contain a date header
|
||||
|
||||
### Bugfix
|
||||
- Fix JavaScript error when adding the first tag manually
|
||||
|
||||
### Feature
|
||||
- OpenAPI / Swagger schema
|
||||
- Download raw message, HTML/text body parts or attachments via single button
|
||||
- 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.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]
|
||||
|
||||
### API
|
||||
- Add limit and start parameters to search
|
||||
@@ -11,7 +281,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
|
||||
@@ -23,14 +293,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
|
||||
@@ -43,13 +313,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
|
||||
@@ -58,7 +328,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
|
||||
@@ -67,7 +337,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
|
||||
@@ -81,32 +351,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
|
||||
@@ -122,7 +392,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
|
||||
|
||||
@@ -135,7 +405,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
|
||||
@@ -145,7 +415,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
|
||||
@@ -158,7 +428,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
|
||||
@@ -173,7 +443,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)
|
||||
@@ -184,19 +454,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
|
||||
@@ -208,7 +478,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
|
||||
@@ -217,25 +487,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
|
||||
@@ -252,13 +522,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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
45
README.md
@@ -6,7 +6,7 @@
|
||||

|
||||
[](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 & API for developers.
|
||||
|
||||
It acts as both an SMTP server, and provides a web interface to view all captured emails.
|
||||
|
||||
@@ -19,35 +19,58 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
|
||||
|
||||
- Runs entirely from a single binary, no installation required
|
||||
- SMTP server (default `0.0.0.0:1025`)
|
||||
- Web UI to view emails (formatted HTML, highlighted HTML source, text, raw source and MIME attachments including image thumbnails)
|
||||
- Web UI to view emails (formatted HTML, highlighted HTML source, text, headers, raw source and MIME attachments including image thumbnails)
|
||||
- Mobile and tablet HTML preview toggle in desktop mode
|
||||
- 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))
|
||||
- Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size, easily handling tens of thousands of emails
|
||||
- SMTP relaying / message release - relay messages via a different SMTP server ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-relay))
|
||||
- Optional SMTP with STARTTLS & SMTP authentication, including an "accept anything" mode ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication))
|
||||
- Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS))
|
||||
- Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication))
|
||||
- A simple REST API 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
|
||||
|
||||
@@ -67,8 +90,8 @@ You can build a Mailpit-specific sendmail binary from source (see [building from
|
||||
|
||||
## Why rewrite MailHog?
|
||||
|
||||
I had been using MailHog for a few years to intercept and test emails generated from several projects. MailHog has a number of severe performance issues, many of the modules are horribly out of date, and other than a few accepted MRs, it is not actively developed.
|
||||
I had been using MailHog for a few years to intercept and test emails generated from several projects. MailHog has a number of performance issues, many of the frontend and Go modules are horribly out of date, and it is not actively developed.
|
||||
|
||||
Initially I started trying to upgrade a fork of MailHog (both the UI as well as the HTTP server & API), but soon discovered that it is (with all due respect) very poorly designed. It is over-engineered (split over 9 separate projects) and has too many unnecessary features for my purpose. It performs exceptionally poorly when dealing with large amounts of emails or processing any email with an attachment (a single email with a 3MB attachment can take over a minute to ingest). The API also transmits a lot of duplicate and unnecessary data on every message request for all web calls, and there is no HTTP compression.
|
||||
Initially I tried to upgrade a fork of MailHog (both the UI as well as the HTTP server & API), but soon discovered that it is (with all due respect to its authors) poorly designed. It is in my opinion over-engineered (split over 9 separate projects), and performs very poorly when dealing with large amounts of emails or processing emails with an attachments (a single email with a 3MB attachment can take over a minute to ingest). Finally, the API transmits a lot of duplicate and unnecessary data on every browser request, and there is no HTTP compression.
|
||||
|
||||
In order to improve it I felt it needed to be completely rewritten, and so Mailpit was born.
|
||||
|
||||
199
cmd/root.go
@@ -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,70 @@ 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().StringVar(&server.AccessControlAllowOrigin, "api-cors", server.AccessControlAllowOrigin, "Set API CORS Access-Control-Allow-Origin header")
|
||||
rootCmd.Flags().BoolVar(&config.UseMessageDates, "use-message-dates", config.UseMessageDates, "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", config.SMTPAuthAcceptAny, "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", config.SMTPAuthAllowInsecure, "Enable insecure PLAIN & LOGIN authentication")
|
||||
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters")
|
||||
|
||||
rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages")
|
||||
rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)")
|
||||
|
||||
rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
|
||||
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
|
||||
|
||||
// deprecated flags 2022/08/06
|
||||
rootCmd.Flags().StringVarP(&config.UIAuthFile, "auth-file", "a", config.UIAuthFile, "A password file for web UI authentication")
|
||||
rootCmd.Flags().StringVar(&config.UITLSCert, "ssl-cert", config.UITLSCert, "SSL certificate - requires ssl-key")
|
||||
rootCmd.Flags().StringVar(&config.UITLSKey, "ssl-key", config.UITLSKey, "SSL key - requires ssl-cert")
|
||||
rootCmd.Flags().Lookup("auth-file").Hidden = true
|
||||
rootCmd.Flags().Lookup("auth-file").Deprecated = "use --ui-auth-file"
|
||||
rootCmd.Flags().Lookup("ssl-cert").Hidden = true
|
||||
rootCmd.Flags().Lookup("ssl-cert").Deprecated = "use --ui-tls-cert"
|
||||
rootCmd.Flags().Lookup("ssl-key").Hidden = true
|
||||
rootCmd.Flags().Lookup("ssl-key").Deprecated = "use --ui-tls-key"
|
||||
|
||||
// deprecated flags 2022/08/30
|
||||
rootCmd.Flags().StringVar(&config.DataFile, "data", config.DataFile, "Database file to store persistent data")
|
||||
rootCmd.Flags().Lookup("data").Hidden = true
|
||||
rootCmd.Flags().Lookup("data").Deprecated = "use --db-file"
|
||||
|
||||
// deprecated flags 2023/03/12
|
||||
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-ssl-cert", config.UITLSCert, "SSL certificate for web UI - requires ui-ssl-key")
|
||||
rootCmd.Flags().StringVar(&config.UITLSKey, "ui-ssl-key", config.UITLSKey, "SSL key for web UI - requires ui-ssl-cert")
|
||||
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 +151,114 @@ 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
|
||||
}
|
||||
|
||||
// Relay server config
|
||||
if len(os.Getenv("MP_SMTP_RELAY_CONFIG")) > 0 {
|
||||
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
|
||||
}
|
||||
if getEnabledFromEnv("MP_SMTP_RELAY_ALL") {
|
||||
config.SMTPRelayAllIncoming = true
|
||||
}
|
||||
|
||||
// Misc options
|
||||
if len(os.Getenv("MP_WEBROOT")) > 0 {
|
||||
config.Webroot = os.Getenv("MP_WEBROOT")
|
||||
}
|
||||
if len(os.Getenv("MP_API_CORS")) > 0 {
|
||||
server.AccessControlAllowOrigin = os.Getenv("MP_API_CORS")
|
||||
}
|
||||
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
|
||||
config.UseMessageDates = true
|
||||
}
|
||||
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
|
||||
config.UseMessageDates = true
|
||||
}
|
||||
if getEnabledFromEnv("MP_QUIET") {
|
||||
logger.QuietLogging = true
|
||||
}
|
||||
if getEnabledFromEnv("MP_VERBOSE") {
|
||||
logger.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
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ var (
|
||||
|
||||
// sendmailCmd represents the sendmail command
|
||||
var sendmailCmd = &cobra.Command{
|
||||
Use: "sendmail",
|
||||
Short: "A sendmail command replacement",
|
||||
Long: `A sendmail command replacement.
|
||||
Use: "sendmail [flags] [recipients]",
|
||||
Short: "A sendmail command replacement for Mailpit",
|
||||
Long: `A sendmail command replacement for Mailpit.
|
||||
|
||||
You can optionally create a symlink called 'sendmail' to the main binary.`,
|
||||
You can optionally create a symlink called 'sendmail' to the Mailpit binary.`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
sendmail.Run()
|
||||
},
|
||||
@@ -26,8 +26,12 @@ func init() {
|
||||
rootCmd.AddCommand(sendmailCmd)
|
||||
|
||||
// these are simply repeated for cli consistency
|
||||
sendmailCmd.Flags().StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender")
|
||||
sendmailCmd.Flags().StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
|
||||
sendmailCmd.Flags().StringVarP(&fromAddr, "from", "f", "", "SMTP sender")
|
||||
sendmailCmd.Flags().BoolVarP(&sendmail.Verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
|
||||
sendmailCmd.Flags().BoolP("long-b", "b", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-i", "i", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-o", "o", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-s", "s", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
sendmailCmd.Flags().BoolP("long-t", "t", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
230
config/config.go
@@ -1,21 +1,27 @@
|
||||
// Package config handles the application configuration
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/tg123/go-htpasswd"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// SMTPListen to listen on <interface>:<port>
|
||||
SMTPListen = "0.0.0.0:1025"
|
||||
SMTPListen = "[::]:1025"
|
||||
|
||||
// HTTPListen to listen on <interface>:<port>
|
||||
HTTPListen = "0.0.0.0:8025"
|
||||
HTTPListen = "[::]:8025"
|
||||
|
||||
// DataFile for mail (optional)
|
||||
DataFile string
|
||||
@@ -23,20 +29,14 @@ var (
|
||||
// MaxMessages is the maximum number of messages a mailbox can have (auto-pruned every minute)
|
||||
MaxMessages = 500
|
||||
|
||||
// VerboseLogging for console output
|
||||
VerboseLogging = false
|
||||
// UseMessageDates sets the Created date using the message date, not the delivered date
|
||||
UseMessageDates bool
|
||||
|
||||
// QuietLogging for console output (errors only)
|
||||
QuietLogging = false
|
||||
// UITLSCert file
|
||||
UITLSCert string
|
||||
|
||||
// NoLogging for tests
|
||||
NoLogging = false
|
||||
|
||||
// UISSLCert file
|
||||
UISSLCert string
|
||||
|
||||
// UISSLKey file
|
||||
UISSLKey string
|
||||
// UITLSKey file
|
||||
UITLSKey string
|
||||
|
||||
// UIAuthFile for basic authentication
|
||||
UIAuthFile string
|
||||
@@ -44,17 +44,48 @@ 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
|
||||
|
||||
// SMTPAuth used for euthentication
|
||||
SMTPAuth *htpasswd.File
|
||||
// SMTPAuthConfig used for authentication auto-generated from SMTPAuthFile
|
||||
SMTPAuthConfig *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 []AutoTag
|
||||
|
||||
// SMTPRelayConfigFile to parse a yaml file and store config of relay SMTP server
|
||||
SMTPRelayConfigFile string
|
||||
|
||||
// SMTPRelayConfig to parse a yaml file and store config of relay SMTP server
|
||||
SMTPRelayConfig smtpRelayConfigStruct
|
||||
|
||||
// ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile
|
||||
ReleaseEnabled = false
|
||||
|
||||
// SMTPRelayAllIncoming is whether to relay all incoming messages via preconfgured SMTP server.
|
||||
// Use with extreme caution!
|
||||
SMTPRelayAllIncoming = false
|
||||
|
||||
// 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,13 +100,32 @@ var (
|
||||
RepoBinaryName = "mailpit"
|
||||
)
|
||||
|
||||
// AutoTag struct for auto-tagging
|
||||
type AutoTag struct {
|
||||
Tag string
|
||||
Match string
|
||||
}
|
||||
|
||||
// SMTPRelayConfigStruct struct for parsing yaml & storing variables
|
||||
type smtpRelayConfigStruct struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
STARTTLS bool `yaml:"starttls"`
|
||||
AllowInsecure bool `yaml:"allow-insecure"`
|
||||
Auth string `yaml:"auth"` // none, plain, cram-md5
|
||||
Username string `yaml:"username"` // plain & cram-md5
|
||||
Password string `yaml:"password"` // plain
|
||||
Secret string `yaml:"secret"` // cram-md5
|
||||
ReturnPath string `yaml:"return-path"` // allows overriding the boune address
|
||||
}
|
||||
|
||||
// VerifyConfig wil do some basic checking
|
||||
func VerifyConfig() error {
|
||||
if DataFile != "" && isDir(DataFile) {
|
||||
DataFile = filepath.Join(DataFile, "mailpit.db")
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`^[a-zA-Z0-9\.\-]{3,}:\d{2,}$`)
|
||||
re := regexp.MustCompile(`^((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(\[([\da-fA-F:])+\])):\d+$`)
|
||||
if !re.MatchString(SMTPListen) {
|
||||
return errors.New("SMTP bind should be in the format of <ip>:<port>")
|
||||
}
|
||||
@@ -95,31 +145,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,17 +178,121 @@ 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SMTPAuth = a
|
||||
SMTPAuthConfig = 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 = []AutoTag{}
|
||||
|
||||
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, AutoTag{Tag: tag, Match: match})
|
||||
} else {
|
||||
return fmt.Errorf("Error parsing tags (%s)", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := parseRelayConfig(SMTPRelayConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ReleaseEnabled && SMTPRelayAllIncoming {
|
||||
return errors.New("SMTP relay config must be set to relay all messages")
|
||||
}
|
||||
|
||||
if SMTPRelayAllIncoming {
|
||||
// this deserves a warning
|
||||
logger.Log().Warnf("[smtp] enabling automatic relay of all new messages via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse & validate the SMTPRelayConfigFile (if set)
|
||||
func parseRelayConfig(c string) error {
|
||||
if c == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isFile(c) {
|
||||
return fmt.Errorf("SMTP relay configuration not found: %s", SMTPRelayConfigFile)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &SMTPRelayConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if SMTPRelayConfig.Host == "" {
|
||||
return errors.New("SMTP relay host not set")
|
||||
}
|
||||
|
||||
if SMTPRelayConfig.Port == 0 {
|
||||
SMTPRelayConfig.Port = 25 // default
|
||||
}
|
||||
|
||||
SMTPRelayConfig.Auth = strings.ToLower(SMTPRelayConfig.Auth)
|
||||
|
||||
if SMTPRelayConfig.Auth == "" || SMTPRelayConfig.Auth == "none" || SMTPRelayConfig.Auth == "false" {
|
||||
SMTPRelayConfig.Auth = "none"
|
||||
} else if SMTPRelayConfig.Auth == "plain" {
|
||||
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
|
||||
return fmt.Errorf("SMTP relay host username or password not set for PLAIN authentication (%s)", c)
|
||||
}
|
||||
} else if strings.HasPrefix(SMTPRelayConfig.Auth, "cram") {
|
||||
SMTPRelayConfig.Auth = "cram-md5"
|
||||
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Secret == "" {
|
||||
return fmt.Errorf("SMTP relay host username or secret not set for CRAM-MD5 authentication (%s)", c)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("SMTP relay authentication method not supported: %s", SMTPRelayConfig.Auth)
|
||||
}
|
||||
|
||||
ReleaseEnabled = true
|
||||
|
||||
logger.Log().Infof("[smtp] enabling message relaying via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ Returns a JSON summary of the message and attachments.
|
||||
"Address": "jane@example.com"
|
||||
}
|
||||
],
|
||||
"Cc": null,
|
||||
"Bcc": null,
|
||||
"Cc": [],
|
||||
"Bcc": [],
|
||||
"ReplyTo": [],
|
||||
"Subject": "Message subject",
|
||||
"Date": "2016-09-07T16:46:00+13:00",
|
||||
"Text": "Plain text MIME part of the email",
|
||||
@@ -57,7 +58,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`, `ReplyTo` - 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -4,8 +4,11 @@ Mailpit provides a simple REST API to access and delete stored messages.
|
||||
|
||||
If the Mailpit server is set to use Basic Authentication, then API requests must use Basic Authentication too.
|
||||
|
||||
You can view the Swagger API documentation directly within Mailpit by going to `http://0.0.0.0:8025/api/v1/`.
|
||||
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Method** : `GET`
|
||||
|
||||
The search returns up to 200 of the most recent matches, and does not support pagination or limits.
|
||||
The search returns the most recent matches (default 50).
|
||||
Matching messages are returned in the order of latest received to oldest.
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -62,8 +62,8 @@ Matching messages are returned in the order of latest received to oldest.
|
||||
|
||||
- `total` - Total messages in mailbox (all messages, not search)
|
||||
- `unread` - Total unread messages in mailbox (all messages, not search)
|
||||
- `count` - Number of messages returned in request (up to 200 for search)
|
||||
- `start` - Always 0 (offset in search is unsupported)
|
||||
- `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
@@ -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`
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 83 KiB |
@@ -1,22 +0,0 @@
|
||||
const { build } = require('esbuild')
|
||||
const pluginVue = require('esbuild-plugin-vue-next')
|
||||
const { sassPlugin } = require('esbuild-sass-plugin');
|
||||
|
||||
const doWatch = process.env.WATCH == 'true' ? true : false;
|
||||
const doMinify = process.env.MINIFY == 'true' ? true : false;
|
||||
|
||||
build({
|
||||
entryPoints: ["server/ui-src/app.js"],
|
||||
bundle: true,
|
||||
watch: doWatch,
|
||||
minify: doMinify,
|
||||
sourcemap: false,
|
||||
outfile: "server/ui/dist/app.js",
|
||||
plugins: [pluginVue(), sassPlugin()],
|
||||
loader: {
|
||||
".svg": "file",
|
||||
".woff": "file",
|
||||
".woff2": "file",
|
||||
},
|
||||
logLevel: "info"
|
||||
})
|
||||
33
esbuild.config.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
|
||||
const ctx = await esbuild.context(
|
||||
{
|
||||
entryPoints: [
|
||||
"server/ui-src/app.js",
|
||||
"server/ui-src/docs.js"
|
||||
],
|
||||
bundle: true,
|
||||
minify: doMinify,
|
||||
sourcemap: false,
|
||||
outdir: "server/ui/dist/",
|
||||
plugins: [pluginVue(), sassPlugin()],
|
||||
loader: {
|
||||
".svg": "file",
|
||||
".woff": "file",
|
||||
".woff2": "file",
|
||||
},
|
||||
logLevel: "info"
|
||||
}
|
||||
)
|
||||
|
||||
if (doWatch) {
|
||||
await ctx.watch()
|
||||
} else {
|
||||
await ctx.rebuild()
|
||||
ctx.dispose()
|
||||
}
|
||||
53
go.mod
@@ -8,55 +8,56 @@ 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/leporo/sqlf v1.3.0
|
||||
github.com/jhillyerd/enmime v0.11.1
|
||||
github.com/k3a/html2text v1.1.0
|
||||
github.com/klauspost/compress v1.16.5
|
||||
github.com/leporo/sqlf v1.4.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.7.0
|
||||
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.9.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.21.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // 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/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // 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.8.0 // indirect
|
||||
golang.org/x/image v0.7.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
lukechampine.com/uint128 v1.3.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.4 // 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
|
||||
)
|
||||
|
||||
176
go.sum
@@ -1,7 +1,7 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
|
||||
github.com/GuiaBolso/darwin v0.0.0-20191218124601-fd6d2aa3d244 h1:dqzm54OhCqY8RinR/cx+Ppb0y56Ds5I3wwWhx4XybDg=
|
||||
github.com/GuiaBolso/darwin v0.0.0-20191218124601-fd6d2aa3d244/go.mod h1:3sqgkckuISJ5rs1EpOp6vCvwOUKe/z9vPmyuIlq8Q/A=
|
||||
github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E=
|
||||
@@ -35,18 +35,16 @@ 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=
|
||||
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
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 +53,20 @@ 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/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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jhillyerd/enmime v0.11.1 h1:U6ToGVxfxNQQhKrAaGxtwOf7Zqksb8AQ3j1CyAWOk5k=
|
||||
github.com/jhillyerd/enmime v0.11.1/go.mod h1:EktNOa/V6ka9yCrfoB2uxgefp1lno6OVdszW0iQ5LnM=
|
||||
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.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/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=
|
||||
@@ -78,19 +74,17 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
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/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c=
|
||||
github.com/leporo/sqlf v1.4.0/go.mod h1:pgN9yKsAnQ+2ewhbZogr98RcasUjPsHF3oXwPPhHvBw=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
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 +94,11 @@ 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
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.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 +110,103 @@ 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.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
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.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
|
||||
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
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/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||
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=
|
||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
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.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ=
|
||||
modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
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.2 h1:ixuUG0QS413Vfzyx6FWx6PYTmHaOegTY+hjzhn7L+a0=
|
||||
modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0=
|
||||
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=
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
3097
package-lock.json
generated
12
package.json
@@ -3,23 +3,25 @@
|
||||
"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",
|
||||
"rapidoc": "^9.3.4",
|
||||
"tinycon": "^0.6.8",
|
||||
"vue": "^3.2.13"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package cmd is the sendmail cli
|
||||
package cmd
|
||||
|
||||
/**
|
||||
@@ -13,10 +14,18 @@ import (
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
// Verbose flag
|
||||
Verbose bool
|
||||
|
||||
fromAddr string
|
||||
)
|
||||
|
||||
// Run the Mailpit sendmail replacement.
|
||||
func Run() {
|
||||
host, err := os.Hostname()
|
||||
@@ -30,7 +39,10 @@ func Run() {
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
fromAddr := username + "@" + host
|
||||
if fromAddr == "" {
|
||||
fromAddr = username + "@" + host
|
||||
}
|
||||
|
||||
smtpAddr := "localhost:1025"
|
||||
var recip []string
|
||||
|
||||
@@ -42,21 +54,32 @@ func Run() {
|
||||
fromAddr = os.Getenv("MP_SENDMAIL_FROM")
|
||||
}
|
||||
|
||||
var verbose bool
|
||||
|
||||
// override defaults from cli flags
|
||||
flag.StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender address")
|
||||
flag.StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
|
||||
flag.StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender")
|
||||
flag.BoolP("long-i", "i", true, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-t", "t", true, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolVarP(&verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
|
||||
flag.BoolVarP(&Verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
|
||||
flag.BoolP("long-b", "b", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-i", "i", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-o", "o", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-s", "s", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.BoolP("long-t", "t", false, "Ignored. This flag exists for sendmail compatibility.")
|
||||
flag.CommandLine.SortFlags = false
|
||||
|
||||
// set the default help
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("A sendmail command replacement for Mailpit (%s).\n\n", config.Version)
|
||||
fmt.Printf("Usage:\n %s [flags] [recipients]\n", os.Args[0])
|
||||
fmt.Println("\nFlags:")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// allow recipient to be passed as an argument
|
||||
recip = flag.Args()
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintln(os.Stderr, smtpAddr, fromAddr)
|
||||
if Verbose {
|
||||
fmt.Fprintln(os.Stdout, smtpAddr, fromAddr)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(os.Stdin)
|
||||
@@ -71,13 +94,30 @@ func Run() {
|
||||
os.Exit(11)
|
||||
}
|
||||
|
||||
if len(recip) == 0 {
|
||||
// We only need to parse the message to get a recipient if none where
|
||||
// provided on the command line.
|
||||
recip = append(recip, msg.Header.Get("To"))
|
||||
addresses := []string{}
|
||||
|
||||
if len(recip) > 0 {
|
||||
addresses = recip
|
||||
} else {
|
||||
// get all recipients in To, Cc and Bcc
|
||||
if to, err := msg.Header.AddressList("To"); err == nil {
|
||||
for _, a := range to {
|
||||
addresses = append(addresses, a.Address)
|
||||
}
|
||||
}
|
||||
if cc, err := msg.Header.AddressList("Cc"); err == nil {
|
||||
for _, a := range cc {
|
||||
addresses = append(addresses, a.Address)
|
||||
}
|
||||
}
|
||||
if bcc, err := msg.Header.AddressList("Bcc"); err == nil {
|
||||
for _, a := range bcc {
|
||||
addresses = append(addresses, a.Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = smtp.SendMail(smtpAddr, nil, fromAddr, recip, body)
|
||||
err = smtp.SendMail(smtpAddr, nil, fromAddr, addresses, body)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error sending mail")
|
||||
logger.Log().Fatal(err)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -9,21 +10,44 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/server/smtpd"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/axllent/mailpit/utils/tools"
|
||||
"github.com/gorilla/mux"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
// swagger:route GET /api/v1/messages messages GetMessages
|
||||
//
|
||||
// # List messages
|
||||
//
|
||||
// Returns messages from the mailbox ordered from newest to oldest.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: start
|
||||
// in: query
|
||||
// description: pagination offset
|
||||
// required: false
|
||||
// type: integer
|
||||
// default: 0
|
||||
// + name: limit
|
||||
// in: query
|
||||
// description: limit results
|
||||
// required: false
|
||||
// type: integer
|
||||
// default: 50
|
||||
//
|
||||
// Responses:
|
||||
// 200: MessagesSummaryResponse
|
||||
// default: ErrorResponse
|
||||
start, limit := getStartLimit(r)
|
||||
|
||||
messages, err := storage.List(start, limit)
|
||||
@@ -34,24 +58,52 @@ 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")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
|
||||
// Search returns up to 200 of the latest messages as JSON
|
||||
// Search returns the latest messages as JSON
|
||||
func Search(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/search messages MessagesSummary
|
||||
//
|
||||
// # Search messages
|
||||
//
|
||||
// Returns the latest messages matching a search.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: query
|
||||
// in: query
|
||||
// description: search query
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: limit
|
||||
// in: query
|
||||
// description: limit results
|
||||
// required: false
|
||||
// type: integer
|
||||
// default: 50
|
||||
//
|
||||
// Responses:
|
||||
// 200: MessagesSummaryResponse
|
||||
// default: ErrorResponse
|
||||
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||
if search == "" {
|
||||
fourOFour(w)
|
||||
httpError(w, "Error: no search query")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,28 +117,51 @@ 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 Message as JSON
|
||||
func GetMessage(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/message/{ID} message Message
|
||||
//
|
||||
// # Get message summary
|
||||
//
|
||||
// Returns the summary of a message, marking the message as read.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
// Responses:
|
||||
// 200: Message
|
||||
// default: ErrorResponse
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
msg, err := storage.GetMessage(id)
|
||||
if err != nil {
|
||||
httpError(w, "Message not found")
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,6 +172,35 @@ func Message(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// DownloadAttachment (method: GET) returns the attachment data
|
||||
func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/message/{ID}/part/{PartID} message Attachment
|
||||
//
|
||||
// # Get message attachment
|
||||
//
|
||||
// This will return the attachment part using the appropriate Content-Type.
|
||||
//
|
||||
// Produces:
|
||||
// - application/*
|
||||
// - image/*
|
||||
// - text/*
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: PartID
|
||||
// in: path
|
||||
// description: attachment part id
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
// Responses:
|
||||
// 200: BinaryResponse
|
||||
// default: ErrorResponse
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
@@ -104,7 +208,7 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
a, err := storage.GetAttachmentPart(id, partID)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
fileName := a.FileName
|
||||
@@ -117,27 +221,48 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(a.Content)
|
||||
}
|
||||
|
||||
// Headers (method: GET) returns the message headers as JSON
|
||||
func Headers(w http.ResponseWriter, r *http.Request) {
|
||||
// GetHeaders (method: GET) returns the message headers as JSON
|
||||
func GetHeaders(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/message/{ID}/headers message Headers
|
||||
//
|
||||
// # Get message headers
|
||||
//
|
||||
// Returns the message headers as an array.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
// Responses:
|
||||
// 200: MessageHeaders
|
||||
// default: ErrorResponse
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
data, err := storage.GetMessageRaw(id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
reader := strings.NewReader(string(data))
|
||||
reader := bytes.NewReader(data)
|
||||
m, err := mail.ReadMessage(reader)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
headers := m.Header
|
||||
bytes, _ := json.Marshal(headers)
|
||||
bytes, _ := json.Marshal(m.Header)
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
@@ -145,6 +270,28 @@ func Headers(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// DownloadRaw (method: GET) returns the full email source as plain text
|
||||
func DownloadRaw(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/message/{ID}/raw message Raw
|
||||
//
|
||||
// # Get message source
|
||||
//
|
||||
// Returns the full email source as plain text.
|
||||
//
|
||||
// Produces:
|
||||
// - text/plain
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
// Responses:
|
||||
// 200: TextResponse
|
||||
// default: ErrorResponse
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
@@ -153,7 +300,7 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
data, err := storage.GetMessageRaw(id)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -165,8 +312,32 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// DeleteMessages (method: DELETE) deletes all messages matching IDS.
|
||||
// If no IDs are provided then all messages are deleted.
|
||||
func DeleteMessages(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route DELETE /api/v1/messages messages Delete
|
||||
//
|
||||
// # Delete messages
|
||||
//
|
||||
// If no IDs are provided then all messages are deleted.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - text/plain
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ids
|
||||
// in: body
|
||||
// description: Message ids to delete
|
||||
// required: false
|
||||
// type: DeleteRequest
|
||||
//
|
||||
// Responses:
|
||||
// 200: OKResponse
|
||||
// default: ErrorResponse
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var data struct {
|
||||
IDs []string
|
||||
@@ -191,7 +362,33 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// SetReadStatus (method: PUT) will update the status to Read/Unread for all provided IDs
|
||||
// If no IDs are provided then all messages are updated.
|
||||
func SetReadStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route PUT /api/v1/messages messages SetReadStatus
|
||||
//
|
||||
// # Set read status
|
||||
//
|
||||
// If no IDs are provided then all messages are updated.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - text/plain
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ids
|
||||
// in: body
|
||||
// description: Message ids to update
|
||||
// required: false
|
||||
// type: SetReadStatusRequest
|
||||
//
|
||||
// Responses:
|
||||
// 200: OKResponse
|
||||
// default: ErrorResponse
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
||||
var data struct {
|
||||
@@ -243,6 +440,181 @@ 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) {
|
||||
// swagger:route PUT /api/v1/tags tags SetTags
|
||||
//
|
||||
// # Set message tags
|
||||
//
|
||||
// To remove all tags from a message, pass an empty tags array.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - text/plain
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ids
|
||||
// in: body
|
||||
// description: Message ids to update
|
||||
// required: true
|
||||
// type: SetTagsRequest
|
||||
//
|
||||
// Responses:
|
||||
// 200: OKResponse
|
||||
// default: ErrorResponse
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
// ReleaseMessage (method: POST) will release a message via a preconfigured external SMTP server.
|
||||
// If no IDs are provided then all messages are updated.
|
||||
func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route POST /api/v1/message/{ID}/release message Release
|
||||
//
|
||||
// # Release message
|
||||
//
|
||||
// Release a message via a preconfigured external SMTP server..
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - text/plain
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: To
|
||||
// in: body
|
||||
// description: Array of email addresses to release message to
|
||||
// required: true
|
||||
// type: ReleaseMessageRequest
|
||||
//
|
||||
// Responses:
|
||||
// 200: OKResponse
|
||||
// default: ErrorResponse
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
msg, err := storage.GetMessageRaw(id)
|
||||
if err != nil {
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
||||
data := releaseMessageRequest{}
|
||||
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tos := data.To
|
||||
if len(tos) == 0 {
|
||||
httpError(w, "No valid addresses found")
|
||||
return
|
||||
}
|
||||
|
||||
for _, to := range tos {
|
||||
if _, err := mail.ParseAddress(to); err != nil {
|
||||
httpError(w, "Invalid email address: "+to)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(msg)
|
||||
m, err := mail.ReadMessage(reader)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
froms, err := m.Header.AddressList("From")
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
from := froms[0].Address
|
||||
|
||||
// if sender is used, then change from to the sender
|
||||
if senders, err := m.Header.AddressList("Sender"); err == nil {
|
||||
from = senders[0].Address
|
||||
}
|
||||
|
||||
msg, err = tools.RemoveMessageHeaders(msg, []string{"Bcc", "Message-Id"})
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// set the Return-Path and SMTP mfrom
|
||||
if config.SMTPRelayConfig.ReturnPath != "" {
|
||||
if m.Header.Get("Return-Path") != "<"+config.SMTPRelayConfig.ReturnPath+">" {
|
||||
msg, err = tools.RemoveMessageHeaders(msg, []string{"Return-Path"})
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
msg = append([]byte("Return-Path: <"+config.SMTPRelayConfig.ReturnPath+">\r\n"), msg...)
|
||||
}
|
||||
|
||||
from = config.SMTPRelayConfig.ReturnPath
|
||||
}
|
||||
|
||||
// generate unique ID
|
||||
uid := uuid.NewV4().String() + "@mailpit"
|
||||
// add unique ID
|
||||
msg = append([]byte("Message-Id: <"+uid+">\r\n"), msg...)
|
||||
|
||||
if err := smtpd.Send(from, tos, msg); err != nil {
|
||||
logger.Log().Errorf("[smtp] error sending message: %s", err.Error())
|
||||
httpError(w, "SMTP error: "+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")
|
||||
|
||||
@@ -8,24 +8,51 @@ 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 {
|
||||
Version string
|
||||
// Response includes the current and latest Mailpit version, database info, and memory usage
|
||||
//
|
||||
// swagger:model AppInformation
|
||||
type appInformation struct {
|
||||
// Current Mailpit version
|
||||
Version string
|
||||
// Latest Mailpit version
|
||||
LatestVersion string
|
||||
Database string
|
||||
DatabaseSize int64
|
||||
Messages int
|
||||
Memory uint64
|
||||
// Database path
|
||||
Database string
|
||||
// Database size in bytes
|
||||
DatabaseSize int64
|
||||
// Total number of messages in the database
|
||||
Messages int
|
||||
// Current memory usage in bytes
|
||||
Memory uint64
|
||||
}
|
||||
|
||||
// AppInfo returns some basic details about the running app, and latest release.
|
||||
func AppInfo(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
info := appVersion{}
|
||||
// swagger:route GET /api/v1/info application AppInformation
|
||||
//
|
||||
// # Get application information
|
||||
//
|
||||
// Returns basic runtime information, message totals and latest release version.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Responses:
|
||||
// 200: InfoResponse
|
||||
// default: ErrorResponse
|
||||
info := appInformation{}
|
||||
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 +67,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")
|
||||
|
||||
39
server/apiv1/structs.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"github.com/axllent/mailpit/storage"
|
||||
)
|
||||
|
||||
// MessagesSummary is a summary of a list of messages
|
||||
type MessagesSummary struct {
|
||||
// Total number of messages in mailbox
|
||||
Total int `json:"total"`
|
||||
|
||||
// Total number of unread messages in mailbox
|
||||
Unread int `json:"unread"`
|
||||
|
||||
// Number of results returned
|
||||
Count int `json:"count"`
|
||||
|
||||
// Pagination offset
|
||||
Start int `json:"start"`
|
||||
|
||||
// All current tags
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
// Messages summary
|
||||
// in:body
|
||||
Messages []storage.MessageSummary `json:"messages"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Message data
|
||||
type Message = storage.Message
|
||||
|
||||
// Attachment summary
|
||||
type Attachment = storage.Attachment
|
||||
19
server/apiv1/swagger-config.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
consumes:
|
||||
- application/json
|
||||
info:
|
||||
description: |-
|
||||
OpenAPI 2.0 documentation for [Mailpit](https://github.com/axllent/mailpit).
|
||||
title: Mailpit API
|
||||
contact:
|
||||
name: GitHub
|
||||
url: https://github.com/axllent/mailpit
|
||||
license:
|
||||
name: MIT license
|
||||
url: https://github.com/axllent/mailpit/blob/develop/LICENSE
|
||||
version: "v1"
|
||||
paths: {}
|
||||
produces:
|
||||
- application/json
|
||||
schemes:
|
||||
- http
|
||||
swagger: "2.0"
|
||||
98
server/apiv1/swagger.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package apiv1
|
||||
|
||||
// These structs are for the purpose of defining swagger HTTP responses
|
||||
|
||||
// Application information
|
||||
// swagger:response InfoResponse
|
||||
type infoResponse struct {
|
||||
// Application information
|
||||
Body appInformation
|
||||
}
|
||||
|
||||
// Web UI configuration
|
||||
// swagger:response WebUIConfigurationResponse
|
||||
type webUIConfigurationResponse struct {
|
||||
// Web UI configuration settings
|
||||
Body webUIConfiguration
|
||||
}
|
||||
|
||||
// Message summary
|
||||
// swagger:response MessagesSummaryResponse
|
||||
type messagesSummaryResponse struct {
|
||||
// The message summary
|
||||
// in: body
|
||||
Body MessagesSummary
|
||||
}
|
||||
|
||||
// Message headers
|
||||
// swagger:model MessageHeaders
|
||||
type messageHeaders map[string][]string
|
||||
|
||||
// Delete request
|
||||
// swagger:model DeleteRequest
|
||||
type deleteRequest struct {
|
||||
// ids
|
||||
// in:body
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
|
||||
// Set read status request
|
||||
// swagger:model SetReadStatusRequest
|
||||
type setReadStatusRequest struct {
|
||||
// Read status
|
||||
Read bool `json:"read"`
|
||||
|
||||
// ids
|
||||
// in:body
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
|
||||
// Set tags request
|
||||
// swagger:model SetTagsRequest
|
||||
type setTagsRequest struct {
|
||||
// Tags
|
||||
// in:body
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
// IDs
|
||||
// in:body
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
|
||||
// Release request
|
||||
// swagger:model ReleaseMessageRequest
|
||||
type releaseMessageRequest struct {
|
||||
// To
|
||||
// in:body
|
||||
To []string `json:"to"`
|
||||
}
|
||||
|
||||
// Binary data reponse inherits the attachment's content type
|
||||
// swagger:response BinaryResponse
|
||||
type binaryResponse struct {
|
||||
// in: body
|
||||
Body string
|
||||
}
|
||||
|
||||
// Plain text response
|
||||
// swagger:response TextResponse
|
||||
type textResponse struct {
|
||||
// in: body
|
||||
Body string
|
||||
}
|
||||
|
||||
// Error reponse
|
||||
// swagger:response ErrorResponse
|
||||
type errorResponse struct {
|
||||
// The error message
|
||||
// in: body
|
||||
Body string
|
||||
}
|
||||
|
||||
// Plain text "ok" reponse
|
||||
// swagger:response OKResponse
|
||||
type okResponse struct {
|
||||
// Default reponse
|
||||
// in: body
|
||||
Body string
|
||||
}
|
||||
@@ -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"
|
||||
@@ -24,6 +24,33 @@ var (
|
||||
|
||||
// Thumbnail returns a thumbnail image for an attachment (images only)
|
||||
func Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/message/{ID}/part/{PartID}/thumb message Thumbnail
|
||||
//
|
||||
// # Get an attachment image thumbnail
|
||||
//
|
||||
// This will return a cropped 180x120 JPEG thumbnail of an image attachment.
|
||||
// If the image is smaller than 180x120 then the image is padded. If the attachment is not an image then a blank image is returned.
|
||||
//
|
||||
// Produces:
|
||||
// - image/jpeg
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: PartID
|
||||
// in: path
|
||||
// description: attachment part id
|
||||
// required: true
|
||||
// type: string
|
||||
//
|
||||
// Responses:
|
||||
// 200: BinaryResponse
|
||||
// default: ErrorResponse
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
54
server/apiv1/webui.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
)
|
||||
|
||||
// Response includes global web UI settings
|
||||
//
|
||||
// swagger:model WebUIConfiguration
|
||||
type webUIConfiguration struct {
|
||||
// Message Relay information
|
||||
MessageRelay struct {
|
||||
// Whether message relaying (release) is enabled
|
||||
Enabled bool
|
||||
// The configured SMTP server address
|
||||
SMTPServer string
|
||||
// Enforced Return-Path (if set) for relay bounces
|
||||
ReturnPath string
|
||||
}
|
||||
}
|
||||
|
||||
// WebUIConfig returns configuration settings for the web UI.
|
||||
func WebUIConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/webui application WebUIConfiguration
|
||||
//
|
||||
// # Get web UI configuration
|
||||
//
|
||||
// Returns configuration settings for the web UI.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Responses:
|
||||
// 200: WebUIConfigurationResponse
|
||||
// default: ErrorResponse
|
||||
conf := webUIConfiguration{}
|
||||
|
||||
conf.MessageRelay.Enabled = config.ReleaseEnabled
|
||||
if config.ReleaseEnabled {
|
||||
conf.MessageRelay.SMTPServer = fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(conf)
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
||||
8
server/handlers/k8healthz.go
Normal 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)
|
||||
}
|
||||
17
server/handlers/k8sready.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package server is the HTTP daemon
|
||||
package server
|
||||
|
||||
import (
|
||||
@@ -8,19 +9,27 @@ 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"
|
||||
)
|
||||
|
||||
//go:embed ui
|
||||
var embeddedFS embed.FS
|
||||
|
||||
// AccessControlAllowOrigin CORS policy
|
||||
var AccessControlAllowOrigin string
|
||||
|
||||
// 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,22 +42,36 @@ 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", logger.CleanIP(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", logger.CleanIP(config.HTTPListen), config.Webroot)
|
||||
logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil))
|
||||
}
|
||||
}
|
||||
@@ -57,16 +80,19 @@ 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}/headers", middleWareFunc(apiv1.GetHeaders)).Methods("GET")
|
||||
r.HandleFunc(config.Webroot+"api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
|
||||
r.HandleFunc(config.Webroot+"api/v1/message/{id}/release", middleWareFunc(apiv1.ReleaseMessage)).Methods("POST")
|
||||
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")
|
||||
r.HandleFunc(config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig)).Methods("GET")
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -94,6 +120,11 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
|
||||
if AccessControlAllowOrigin != "" && strings.HasPrefix(r.RequestURI, config.Webroot+"api/") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", AccessControlAllowOrigin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT")
|
||||
}
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
|
||||
@@ -127,6 +158,11 @@ func middlewareHandler(h http.Handler) http.Handler {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
|
||||
if AccessControlAllowOrigin != "" && strings.HasPrefix(r.RequestURI, config.Webroot+"api/") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", AccessControlAllowOrigin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT")
|
||||
}
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
|
||||
@@ -152,6 +188,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)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/server/apiv1"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
@@ -150,7 +151,7 @@ func Test_APIv1(t *testing.T) {
|
||||
}
|
||||
|
||||
func setup() {
|
||||
config.NoLogging = true
|
||||
logger.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
|
||||
@@ -160,7 +161,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 +180,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 +227,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 {
|
||||
|
||||
71
server/smtpd/smtp.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
)
|
||||
|
||||
// Send will connect to a pre-configured SMTP server and send a message to one or more recipients.
|
||||
func Send(from string, to []string, msg []byte) error {
|
||||
addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
if config.SMTPRelayConfig.STARTTLS {
|
||||
conf := &tls.Config{ServerName: config.SMTPRelayConfig.Host}
|
||||
|
||||
conf.InsecureSkipVerify = config.SMTPRelayConfig.AllowInsecure
|
||||
|
||||
if err = c.StartTLS(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var a smtp.Auth
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "plain" {
|
||||
a = smtp.PlainAuth("", config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password, config.SMTPRelayConfig.Host)
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "cram-md5" {
|
||||
a = smtp.CRAMMD5Auth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Secret)
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Quit()
|
||||
}
|
||||
201
server/smtpd/smtpd.go
Normal file
@@ -0,0 +1,201 @@
|
||||
// Package smtpd is the SMTP daemon
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"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"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
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("[smtpd] error parsing message: %s", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// add a message ID if not set
|
||||
if msg.Header.Get("Message-Id") == "" {
|
||||
// generate unique ID
|
||||
uid := uuid.NewV4().String() + "@mailpit"
|
||||
// add unique ID
|
||||
data = append([]byte("Message-Id: <"+uid+">\r\n"), data...)
|
||||
}
|
||||
|
||||
// if enabled, this will route the email 1:1 through to the preconfigured smtp server
|
||||
if config.SMTPRelayAllIncoming {
|
||||
if err := Send(from, to, data); err != nil {
|
||||
logger.Log().Errorf("[smtp] error relaying message: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[smtp] relayed message from %s via %s:%d", from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// build array of all addresses in the header to compare to the []to array
|
||||
emails, hasBccHeader := scanAddressesInHeader(msg.Header)
|
||||
|
||||
missingAddresses := []string{}
|
||||
for _, a := range to {
|
||||
// loop through passed email addresses to check if they are in the headers
|
||||
if _, err := mail.ParseAddress(a); err == nil {
|
||||
_, ok := emails[strings.ToLower(a)]
|
||||
if !ok {
|
||||
missingAddresses = append(missingAddresses, a)
|
||||
}
|
||||
} else {
|
||||
logger.Log().Warnf("[smtpd] ignoring invalid email address: %s", a)
|
||||
}
|
||||
}
|
||||
|
||||
// add missing email addresses to Bcc (eg: Laravel doesn't include these in the headers)
|
||||
if len(missingAddresses) > 0 {
|
||||
if hasBccHeader {
|
||||
// email already has Bcc header, add to existing addresses
|
||||
re := regexp.MustCompile(`(?i)(^|\n)(Bcc: )`)
|
||||
replaced := false
|
||||
data = re.ReplaceAllFunc(data, func(r []byte) []byte {
|
||||
if replaced {
|
||||
return r
|
||||
}
|
||||
replaced = true // only replace first occurence
|
||||
|
||||
return re.ReplaceAll(r, []byte("${1}Bcc: "+strings.Join(missingAddresses, ", ")+", "))
|
||||
})
|
||||
|
||||
} else {
|
||||
// prepend new Bcc header
|
||||
bcc := []byte(fmt.Sprintf("Bcc: %s\r\n", strings.Join(missingAddresses, ", ")))
|
||||
data = append(bcc, data...)
|
||||
}
|
||||
|
||||
logger.Log().Debugf("[smtpd] added missing addresses to Bcc header: %s", strings.Join(missingAddresses, ", "))
|
||||
}
|
||||
|
||||
if _, err := storage.Store(data); err != nil {
|
||||
logger.Log().Errorf("[db] error storing message: %d", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
subject := msg.Header.Get("Subject")
|
||||
logger.Log().Debugf("[smtpd] received (%s) from:%s subject:%q", cleanIP(origin), from, subject)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
|
||||
allow := config.SMTPAuthConfig.Match(string(username), string(password))
|
||||
if allow {
|
||||
logger.Log().Debugf("[smtpd] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
|
||||
} else {
|
||||
logger.Log().Warnf("[smtpd] 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("[smtpd] 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("[smtpd] enabling login auth via %s (insecure)", config.SMTPAuthFile)
|
||||
} else if config.SMTPAuthAcceptAny {
|
||||
logger.Log().Info("[smtpd] enabling all auth (insecure)")
|
||||
}
|
||||
} else {
|
||||
if config.SMTPAuthFile != "" {
|
||||
logger.Log().Infof("[smtpd] enabling login auth via %s (TLS)", config.SMTPAuthFile)
|
||||
} else if config.SMTPAuthAcceptAny {
|
||||
logger.Log().Info("[smtpd] enabling any auth (TLS)")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Infof("[smtpd] starting on %s", logger.CleanIP(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]
|
||||
}
|
||||
|
||||
// Returns a list of all lowercased emails found in To, Cc and Bcc,
|
||||
// as well as whether there is a Bcc field
|
||||
func scanAddressesInHeader(h mail.Header) (map[string]bool, bool) {
|
||||
emails := make(map[string]bool)
|
||||
hasBccHeader := false
|
||||
|
||||
if recipients, err := h.AddressList("To"); err == nil {
|
||||
for _, r := range recipients {
|
||||
emails[strings.ToLower(r.Address)] = true
|
||||
}
|
||||
}
|
||||
|
||||
if recipients, err := h.AddressList("Cc"); err == nil {
|
||||
for _, r := range recipients {
|
||||
emails[strings.ToLower(r.Address)] = true
|
||||
}
|
||||
}
|
||||
|
||||
recipients, err := h.AddressList("Bcc")
|
||||
if err == nil {
|
||||
for _, r := range recipients {
|
||||
emails[strings.ToLower(r.Address)] = true
|
||||
}
|
||||
|
||||
hasBccHeader = true
|
||||
}
|
||||
|
||||
return emails, hasBccHeader
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
<script>
|
||||
import commonMixins from './mixins.js';
|
||||
import Message from './templates/Message.vue';
|
||||
import MessageSummary from './templates/MessageSummary.vue';
|
||||
import MessageRelease from './templates/MessageRelease.vue';
|
||||
import MessageToast from './templates/MessageToast.vue';
|
||||
import moment from 'moment';
|
||||
import Tinycon from 'tinycon';
|
||||
|
||||
export default {
|
||||
mixins: [commonMixins],
|
||||
|
||||
components: {
|
||||
Message
|
||||
Message,
|
||||
MessageSummary,
|
||||
MessageRelease,
|
||||
MessageToast
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentPath: window.location.hash,
|
||||
@@ -18,6 +26,8 @@ export default {
|
||||
unread: 0,
|
||||
start: 0,
|
||||
count: 0,
|
||||
tags: [],
|
||||
existingTags: [], // to pass onto components
|
||||
search: "",
|
||||
searching: false,
|
||||
isConnected: false,
|
||||
@@ -31,8 +41,12 @@ export default {
|
||||
tcStatus: 0,
|
||||
appInfo: false,
|
||||
lastLoaded: false,
|
||||
relayConfig: {},
|
||||
releaseAddresses: false,
|
||||
toastMessage: 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 +67,7 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
canPrev: function () {
|
||||
return this.start > 0;
|
||||
@@ -61,6 +76,7 @@ export default {
|
||||
return this.total > (this.start + this.count);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.currentPath = window.location.hash.slice(1);
|
||||
window.addEventListener('hashchange', () => {
|
||||
@@ -78,21 +94,24 @@ export default {
|
||||
});
|
||||
|
||||
this.connect();
|
||||
this.getUISettings();
|
||||
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 +135,11 @@ export default {
|
||||
self.count = response.data.count;
|
||||
self.start = response.data.start;
|
||||
self.items = response.data.messages;
|
||||
self.tags = response.data.tags;
|
||||
self.existingTags = JSON.parse(JSON.stringify(self.tags));
|
||||
|
||||
if (self.items == 0 && self.start > 0) {
|
||||
// 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 +151,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
self.scrollInPlace = false
|
||||
self.scrollInPlace = false;
|
||||
});
|
||||
},
|
||||
|
||||
getUISettings: function () {
|
||||
let self = this;
|
||||
self.get('api/v1/webui', null, function (response) {
|
||||
self.relayConfig = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -138,6 +167,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,6 +207,9 @@ export default {
|
||||
openMessage: function (id) {
|
||||
let self = this;
|
||||
self.selected = [];
|
||||
self.releaseAddresses = false;
|
||||
self.toastMessage = false;
|
||||
self.existingTags = JSON.parse(JSON.stringify(self.tags));
|
||||
|
||||
let uri = 'api/v1/message/' + self.currentPath
|
||||
self.get(uri, false, function (response) {
|
||||
@@ -179,51 +221,56 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let d = response.data;
|
||||
// replace inline images
|
||||
|
||||
// replace inline images embedded as inline attachments
|
||||
if (d.HTML && d.Inline) {
|
||||
for (let i in d.Inline) {
|
||||
let a = d.Inline[i];
|
||||
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
|
||||
new RegExp('(=["\']?)(cid:' + a.ContentID + ')(["|\'|\s|\/|>|;])', 'g'),
|
||||
'$1' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '$3'
|
||||
);
|
||||
}
|
||||
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 + '"'
|
||||
new RegExp('(=["\']?)(' + a.FileName + ')(["|\'|\s|\/|>|;])', 'g'),
|
||||
'$1' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '$3'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// replace inline images
|
||||
|
||||
// replace inline images embedded as regular attachments
|
||||
if (d.HTML && d.Attachments) {
|
||||
for (let i in d.Attachments) {
|
||||
let a = d.Attachments[i];
|
||||
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
|
||||
new RegExp('(=["\']?)(cid:' + a.ContentID + ')(["|\'|\s|\/|>|;])', 'g'),
|
||||
'$1' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '$3'
|
||||
);
|
||||
}
|
||||
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 + '"'
|
||||
new RegExp('(=["\']?)(' + a.FileName + ')(["|\'|\s|\/|>|;])', 'g'),
|
||||
'$1' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '$3'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.message = d;
|
||||
|
||||
// generate the prev/next links based on current message list
|
||||
self.messagePrev = false;
|
||||
self.messageNext = false;
|
||||
let found = false;
|
||||
|
||||
for (let i in self.items) {
|
||||
if (self.items[i].ID == self.message.ID) {
|
||||
found = true;
|
||||
@@ -357,18 +404,33 @@ export default {
|
||||
if (response.Type == "new" && response.Data) {
|
||||
if (!self.searching) {
|
||||
if (self.start < 1) {
|
||||
// first page
|
||||
self.items.unshift(response.Data);
|
||||
if (self.items.length > self.limit) {
|
||||
self.items.pop();
|
||||
}
|
||||
|
||||
// first message was open, set messagePrev
|
||||
if (!self.messagePrev) {
|
||||
self.messagePrev = response.Data.ID;
|
||||
}
|
||||
} else {
|
||||
self.start++;
|
||||
}
|
||||
}
|
||||
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);
|
||||
self.setMessageToast(response.Data);
|
||||
} else if (response.Type == "prune") {
|
||||
// messages have been deleted, reload messages to adjust
|
||||
self.scrollInPlace = true;
|
||||
@@ -416,7 +478,7 @@ export default {
|
||||
let b = message.Subject;
|
||||
let options = {
|
||||
body: message,
|
||||
icon: 'mailpit.png'
|
||||
icon: 'notification.png'
|
||||
}
|
||||
new Notification(title, options);
|
||||
}
|
||||
@@ -462,7 +524,7 @@ export default {
|
||||
this.selected = this.selected.filter(function (ele) {
|
||||
return ele != id;
|
||||
});
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastSelected === false) {
|
||||
@@ -492,6 +554,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;
|
||||
@@ -499,70 +570,161 @@ export default {
|
||||
self.appInfo = response.data;
|
||||
self.modal('AppInfoModal').show();
|
||||
});
|
||||
},
|
||||
|
||||
downloadMessageBody: function (str, ext) {
|
||||
let dl = document.createElement('a');
|
||||
dl.href = "data:text/plain," + encodeURIComponent(str);
|
||||
dl.target = '_blank';
|
||||
dl.download = this.message.ID + '.' + ext;
|
||||
dl.click();
|
||||
},
|
||||
|
||||
initReleaseModal: function () {
|
||||
this.releaseAddresses = false;
|
||||
let addresses = [];
|
||||
for (let i in this.message.To) {
|
||||
addresses.push(this.message.To[i].Address)
|
||||
}
|
||||
for (let i in this.message.Cc) {
|
||||
addresses.push(this.message.Cc[i].Address)
|
||||
}
|
||||
for (let i in this.message.Bcc) {
|
||||
addresses.push(this.message.Bcc[i].Address)
|
||||
}
|
||||
|
||||
// include only unique email addresses, regardless of casing
|
||||
let uAddresses = new Map(addresses.map(a => [a.toLowerCase(), a]));
|
||||
this.releaseAddresses = [...uAddresses.values()];
|
||||
|
||||
let self = this;
|
||||
window.setTimeout(function () {
|
||||
// delay to allow elements to load
|
||||
self.modal('ReleaseModal').show();
|
||||
|
||||
window.setTimeout(function () {
|
||||
document.querySelector('#ReleaseModal input[role="combobox"]').focus()
|
||||
}, 500);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
setMessageToast: function (m) {
|
||||
if (this.toastMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastMessage = m;
|
||||
},
|
||||
|
||||
clearMessageToast: function () {
|
||||
this.toastMessage = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</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="Release message"
|
||||
v-if="relayConfig.MessageRelay && relayConfig.MessageRelay.Enabled" v-on:click="initReleaseModal">
|
||||
<i class="bi bi-send"></i> <span class="d-none d-md-inline">Release</span>
|
||||
</button>
|
||||
<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.ID + '/raw?dl=1'" class="btn btn-outline-secondary 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 class="dropdown float-end" id="DownloadBtn">
|
||||
<button type="button" class="btn btn-outline-light dropdown-toggle" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-file-arrow-down-fill"></i>
|
||||
<span class="d-none d-md-inline ms-1">Download</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a :href="'api/v1/message/' + message.ID + '/raw?dl=1'" class="dropdown-item"
|
||||
title="Message source including headers, body and attachments">
|
||||
Raw message
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="message.HTML">
|
||||
<button v-on:click="downloadMessageBody(message.HTML, 'html')" class="dropdown-item">
|
||||
HTML body
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="message.Text">
|
||||
<button v-on:click="downloadMessageBody(message.Text, 'txt')" class="dropdown-item">
|
||||
Text body
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="allAttachments(message).length">
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li v-for="part in allAttachments(message)">
|
||||
<a :href="'api/v1/message/' + message.ID + '/part/' + part.PartID" type="button"
|
||||
class="row m-0 dropdown-item d-flex" target="_blank"
|
||||
:title="part.FileName != '' ? part.FileName : '[ unknown ]'" style="min-width: 350px">
|
||||
<div class="col-auto p-0 pe-1">
|
||||
<i class="bi" :class="attachmentIcon(part)"></i>
|
||||
</div>
|
||||
<div class="col text-truncate p-0 pe-1">
|
||||
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
|
||||
</div>
|
||||
<div class="col-auto text-muted small p-0">
|
||||
{{ getFileSize(part.Size) }}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</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">
|
||||
<input type="text" class="form-control border-0" v-model.trim="search"
|
||||
placeholder="Search mailbox">
|
||||
<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
|
||||
class="bi bi-search"></i></button>
|
||||
<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>
|
||||
@@ -574,128 +736,145 @@ export default {
|
||||
<option value="100">100</option>
|
||||
<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 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 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
|
||||
</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 +896,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 +920,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>
|
||||
@@ -749,8 +929,7 @@ export default {
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="MarkAllReadModal" tabindex="-1" aria-labelledby="MarkAllReadModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal fade" id="MarkAllReadModal" tabindex="-1" aria-labelledby="MarkAllReadModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -761,8 +940,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 +966,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,11 +987,17 @@ 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>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<a class="btn btn-primary w-100" href="api/v1/" target="_blank">
|
||||
<i class="bi bi-braces"></i>
|
||||
OpenAPI / Swagger API documentation
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit" target="_blank">
|
||||
<i class="bi bi-github"></i>
|
||||
@@ -821,8 +1006,7 @@ export default {
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki"
|
||||
target="_blank">
|
||||
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki" target="_blank">
|
||||
Documentation
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
@@ -851,4 +1035,12 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="ReleaseModal" tabindex="-1" aria-labelledby="AppInfoModalLabel" aria-hidden="true">
|
||||
<MessageRelease v-if="releaseAddresses" :message="message" :relayConfig="relayConfig"
|
||||
:releaseAddresses="releaseAddresses"></MessageRelease>
|
||||
</div>
|
||||
|
||||
<MessageToast v-if="toastMessage" :message="toastMessage" @clearMessageToast="clearMessageToast"></MessageToast>
|
||||
</template>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
$link-decoration: none;
|
||||
$primary: #3465b5;
|
||||
$primary: #2c3e50;
|
||||
$list-group-disabled-color: #adb5bd;
|
||||
|
||||
2
server/ui-src/assets/bootstrap.scss
vendored
@@ -32,7 +32,7 @@
|
||||
// @import "../../../node_modules/bootstrap/scss/progress";
|
||||
@import "../../../node_modules/bootstrap/scss/list-group";
|
||||
@import "../../../node_modules/bootstrap/scss/close";
|
||||
// @import "../../../node_modules/bootstrap/scss/toasts";
|
||||
@import "../../../node_modules/bootstrap/scss/toasts";
|
||||
@import "../../../node_modules/bootstrap/scss/modal";
|
||||
// @import "../../../node_modules/bootstrap/scss/tooltip";
|
||||
// @import "../../../node_modules/bootstrap/scss/popover";
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
.navbar-brand {
|
||||
color: #2d4a5d;
|
||||
transition: all 0.2s;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
@@ -24,6 +25,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
span {
|
||||
opacity: 0.8;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
@include media-breakpoint-down(sm) {
|
||||
// font-size: 14px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -69,18 +91,100 @@
|
||||
|
||||
#preview-html {
|
||||
min-height: 300px;
|
||||
|
||||
&.tablet,
|
||||
&.phone {
|
||||
border: solid $gray-300 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item:first-child {
|
||||
#responsive-view {
|
||||
margin: auto;
|
||||
transition: width 0.5s;
|
||||
position: relative;
|
||||
|
||||
&.tablet,
|
||||
&.phone {
|
||||
border-radius: 35px;
|
||||
box-sizing: content-box;
|
||||
padding-bottom: 76px;
|
||||
padding-top: 54px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
background: $gray-800;
|
||||
|
||||
iframe {
|
||||
height: 100% !important;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.phone {
|
||||
&::before {
|
||||
border-radius: 5px;
|
||||
background: $gray-600;
|
||||
top: 22px;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 10px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-radius: 20px;
|
||||
background: $gray-900;
|
||||
bottom: 20px;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 65px;
|
||||
height: 40px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&.tablet {
|
||||
&::before {
|
||||
border-radius: 50%;
|
||||
border: solid #b5b0b0 2px;
|
||||
top: 22px;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-radius: 50%;
|
||||
border: solid #b5b0b0 2px;
|
||||
bottom: 23px;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 +240,42 @@ body.blur {
|
||||
}
|
||||
}
|
||||
|
||||
.form-select.tag-selector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-control.dropdown {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
input {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
div {
|
||||
cursor: text; // html5-tags
|
||||
}
|
||||
}
|
||||
|
||||
#DownloadBtn {
|
||||
@include media-breakpoint-down(sm) {
|
||||
position: static;
|
||||
|
||||
.dropdown-menu {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ReleaseModal {
|
||||
.form-control.dropdown {
|
||||
div {
|
||||
@extend .form-control;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* PrismJS 1.29.0 - modified!
|
||||
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css */
|
||||
code[class*="language-"],
|
||||
@@ -257,8 +397,8 @@ pre[class*="language-"] {
|
||||
opacity: 0.7;
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
pre[class*="language-"]:after,
|
||||
pre[class*="language-"]:before {
|
||||
pre[class*="language-"]::after,
|
||||
pre[class*="language-"]::before {
|
||||
bottom: 14px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
1
server/ui-src/docs.js
Normal file
@@ -0,0 +1 @@
|
||||
import "rapidoc";
|
||||
@@ -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;
|
||||
|
||||
38
server/ui-src/templates/Headers.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
<script>
|
||||
import commonMixins from '../mixins.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: Object
|
||||
},
|
||||
|
||||
mixins: [commonMixins],
|
||||
|
||||
data() {
|
||||
return {
|
||||
headers: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let self = this;
|
||||
let uri = 'api/v1/message/' + self.message.ID + '/headers';
|
||||
self.get(uri, false, function (response) {
|
||||
self.headers = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="headers" class="small">
|
||||
<div v-for="vals, k in headers" class="row mb-2 pb-2 border-bottom w-100">
|
||||
<div class="col-md-4 col-lg-3 col-xl-2 mb-2"><b>{{ k }}</b></div>
|
||||
<div class="col-md-8 col-lg-9 col-xl-10 text-muted">
|
||||
<div v-for="x in vals" class="mb-2 text-break">{{ x }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,17 +1,20 @@
|
||||
|
||||
<script>
|
||||
import commonMixins from '../mixins.js';
|
||||
import moment from 'moment';
|
||||
import Prism from "prismjs";
|
||||
import Tags from "bootstrap5-tags";
|
||||
import Attachments from './Attachments.vue';
|
||||
import Headers from './Headers.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: Object
|
||||
message: Object,
|
||||
existingTags: Array
|
||||
},
|
||||
|
||||
components: {
|
||||
Attachments
|
||||
Attachments,
|
||||
Headers
|
||||
},
|
||||
|
||||
mixins: [commonMixins],
|
||||
@@ -20,49 +23,97 @@ export default {
|
||||
return {
|
||||
srcURI: false,
|
||||
iframes: [], // for resizing
|
||||
showTags: false, // to force rerendering of component
|
||||
messageTags: [],
|
||||
allTags: [],
|
||||
loadHeaders: false,
|
||||
showMobileBtns: false,
|
||||
scaleHTMLPreview: 'display',
|
||||
// keys names match bootstrap icon names
|
||||
responsiveSizes: {
|
||||
phone: 'width: 322px; height: 570px',
|
||||
tablet: 'width: 768px; height: 1024px',
|
||||
display: 'width: 100%; height: 100%',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
message: {
|
||||
handler(newQuestion) {
|
||||
handler() {
|
||||
let self = this;
|
||||
// delay 100ms to select first tab and add HTML highlighting (prev/next)
|
||||
window.setTimeout(function() {
|
||||
self.showTags = false;
|
||||
self.messageTags = self.message.Tags;
|
||||
self.allTags = self.existingTags;
|
||||
self.loadHeaders = false;
|
||||
self.scaleHTMLPreview = 'display';// default view
|
||||
// delay to select first tab and add HTML highlighting (prev/next)
|
||||
self.$nextTick(function () {
|
||||
self.renderUI();
|
||||
}, 100)
|
||||
self.showTags = true;
|
||||
self.$nextTick(function () {
|
||||
Tags.init("select[multiple]");
|
||||
});
|
||||
});
|
||||
},
|
||||
// force eager callback execution
|
||||
immediate: true
|
||||
},
|
||||
messageTags() {
|
||||
// save changed to tags
|
||||
if (this.showTags) {
|
||||
this.saveTags();
|
||||
}
|
||||
},
|
||||
scaleHTMLPreview() {
|
||||
if (this.scaleHTMLPreview == 'display') {
|
||||
let self = this;
|
||||
window.setTimeout(function () {
|
||||
self.resizeIframes();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let self = this;
|
||||
self.showTags = false;
|
||||
self.allTags = self.existingTags;
|
||||
window.addEventListener("resize", self.resizeIframes);
|
||||
self.renderUI();
|
||||
var tabEl = document.getElementById('nav-raw-tab');
|
||||
tabEl.addEventListener('shown.bs.tab', function (event) {
|
||||
|
||||
let headersTab = document.getElementById('nav-headers-tab');
|
||||
headersTab.addEventListener('shown.bs.tab', function (event) {
|
||||
self.loadHeaders = true;
|
||||
});
|
||||
|
||||
let rawTab = document.getElementById('nav-raw-tab');
|
||||
rawTab.addEventListener('shown.bs.tab', function (event) {
|
||||
self.srcURI = 'api/v1/message/' + self.message.ID + '/raw';
|
||||
self.resizeIframes();
|
||||
});
|
||||
|
||||
self.showTags = true;
|
||||
self.$nextTick(function () {
|
||||
Tags.init("select[multiple]");
|
||||
});
|
||||
},
|
||||
|
||||
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,26 +134,35 @@ 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 () {
|
||||
if (this.scaleHTMLPreview != 'display') {
|
||||
return;
|
||||
}
|
||||
let h = document.getElementById('preview-html');
|
||||
if (h) {
|
||||
h.style.height = h.contentWindow.document.body.scrollHeight + 50 + 'px';
|
||||
}
|
||||
|
||||
let s = document.getElementById('message-src');
|
||||
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');
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,12 +174,14 @@ export default {
|
||||
<div class="col-md">
|
||||
<table class="messageHeaders">
|
||||
<tbody>
|
||||
<tr class="small">
|
||||
<th>From</th>
|
||||
<tr>
|
||||
<th class="small">From</th>
|
||||
<td class="privacy">
|
||||
<span v-if="message.From">
|
||||
<span v-if="message.From.Name">{{ message.From.Name + " " }}</span>
|
||||
<span v-if="message.From.Address"><{{ message.From.Address }}></span>
|
||||
<span v-if="message.From.Address" class="small">
|
||||
<{{ message.From.Address }}>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
[ Unknown ]
|
||||
@@ -129,100 +191,142 @@ 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 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 +">" }}
|
||||
{{ t.Name + " <" + t.Address + ">" }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.ReplyTo && message.ReplyTo.length" class="small">
|
||||
<th class="text-nowrap">Reply-To</th>
|
||||
<td class="privacy text-muted">
|
||||
<span v-for="(t, i) in message.ReplyTo">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address + ">" }} </span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.ReturnPath && message.ReturnPath != message.From.Address" class="small">
|
||||
<th class="text-nowrap">Return-Path</th>
|
||||
<td class="privacy text-muted"><{{ message.ReturnPath }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="small">Subject</th>
|
||||
<td><strong>{{ message.Subject }}</strong></td>
|
||||
<td>
|
||||
<strong v-if="message.Subject != ''">{{ message.Subject }}</strong>
|
||||
<small class="text-muted" v-else>[ no subject ]</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="d-md-none">
|
||||
<tr class="d-md-none small">
|
||||
<th class="small">Date</th>
|
||||
<td>{{ messageDate(message.Date) }}</td>
|
||||
</tr>
|
||||
|
||||
<tr class="small" v-if="showTags">
|
||||
<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 allTags" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">Please select a valid tag.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</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">
|
||||
<div class="col-md-auto d-none d-md-block text-end mt-md-3">
|
||||
<div class="mt-2 mt-md-0" v-if="allAttachments(message)">
|
||||
<span class="badge rounded-pill text-bg-secondary p-2">
|
||||
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"
|
||||
class="dropdown-item" target="_blank">
|
||||
<i class="bi" :class="attachmentIcon(part)"></i>
|
||||
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
|
||||
<small class="text-muted ms-2">{{ getFileSize(part.Size) }}</small>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
v-on:click="showMobileBtns = true; resizeIframes()">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"
|
||||
v-on:click="showMobileBtns = false">
|
||||
HTML <span class="d-sm-none">Src</span><span class="d-none d-sm-inline">Source</span>
|
||||
</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' : ''" v-on:click="showMobileBtns = false">Text</button>
|
||||
<button class="nav-link" id="nav-headers-tab" data-bs-toggle="tab" data-bs-target="#nav-headers"
|
||||
type="button" role="tab" aria-controls="nav-headers" aria-selected="false"
|
||||
v-on:click="showMobileBtns = false">
|
||||
<span class="d-sm-none">Hdrs</span><span class="d-none d-sm-inline">Headers</span>
|
||||
</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"
|
||||
v-on:click="showMobileBtns = false">Raw</button>
|
||||
|
||||
<div class="d-none d-lg-block ms-auto me-2" v-if="showMobileBtns">
|
||||
<template v-for="vals, key in responsiveSizes">
|
||||
<button class="btn" :class="scaleHTMLPreview == key ? 'btn-outline-primary' : ''"
|
||||
:disabled="scaleHTMLPreview == key" :title="'Switch to ' + key + ' view'"
|
||||
v-on:click="scaleHTMLPreview = key">
|
||||
<i class="bi" :class="'bi-' + key"></i>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</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>
|
||||
<Attachments v-if="allAttachments(message).length" :message="message" :attachments="allAttachments(message)"></Attachments>
|
||||
<div id="responsive-view" :class="scaleHTMLPreview" :style="responsiveSizes[scaleHTMLPreview]">
|
||||
<iframe target-blank="" class="tab-pane d-block" id="preview-html" :srcdoc="message.HTML"
|
||||
v-on:load="resizeIframe" seamless frameborder="0" style="width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
</div>
|
||||
<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-headers" role="tabpanel" aria-labelledby="nav-headers-tab" tabindex="0">
|
||||
<Headers v-if="loadHeaders" :message="message"></Headers>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
109
server/ui-src/templates/MessageRelease.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
<script>
|
||||
import Tags from "bootstrap5-tags";
|
||||
import commonMixins from '../mixins.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: Object,
|
||||
relayConfig: Object,
|
||||
releaseAddresses: Array
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
addresses: []
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [commonMixins],
|
||||
|
||||
mounted() {
|
||||
this.addresses = JSON.parse(JSON.stringify(this.releaseAddresses));
|
||||
this.$nextTick(function () {
|
||||
Tags.init("select[multiple]");
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
releaseMessage: function () {
|
||||
let self = this;
|
||||
// set timeout to allow for user clicking send before the tag filter has applied the tag
|
||||
window.setTimeout(function () {
|
||||
if (!self.addresses.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let data = {
|
||||
to: self.addresses
|
||||
}
|
||||
|
||||
self.post('api/v1/message/' + self.message.ID + '/release', data, function (response) {
|
||||
self.modal("ReleaseModal").hide();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal-dialog modal-lg" v-if="message">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="AppInfoModalLabel">Release email</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>Send this message to one or more addresses specified below.</h6>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label text-muted">From</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" :value="message.From.Address">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class=" col-sm-2 col-form-label text-muted">Subject</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" :value="message.Subject">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label text-muted">Send to</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select tag-selector" v-model="addresses" multiple data-allow-new="true"
|
||||
data-clear-end="true" data-allow-clear="true" data-placeholder="Enter email addresses..."
|
||||
data-add-on-blur="true" data-badge-style="primary"
|
||||
data-regex='^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'
|
||||
data-separator="|,|">
|
||||
<option value="">Enter email addresses...</option>
|
||||
<!-- you need at least one option with the placeholder -->
|
||||
<option v-for="t in releaseAddresses" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">Invalid email address</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text text-center">
|
||||
Note: For testing purposes, a unique Message-Id will be generated on send.
|
||||
<br class="d-none d-md-inline">
|
||||
SMTP delivery failures will bounce back to
|
||||
<b v-if="relayConfig.MessageRelay.ReturnPath != ''">{{ relayConfig.MessageRelay.ReturnPath }}</b>
|
||||
<b v-else>{{ message.ReturnPath }}</b>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" :disabled="!addresses.length"
|
||||
v-on:click="releaseMessage">Release</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loading" v-if="loading">
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
28
server/ui-src/templates/MessageSummary.vue
Normal 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>
|
||||
44
server/ui-src/templates/MessageToast.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script>
|
||||
import { Toast } from 'bootstrap';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: Object
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let self = this;
|
||||
let el = document.getElementById('messageToast');
|
||||
if (el) {
|
||||
el.addEventListener('hidden.bs.toast', () => {
|
||||
self.$emit("clearMessageToast");
|
||||
})
|
||||
|
||||
let b = Toast.getOrCreateInstance(el);
|
||||
b.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="messageToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<i class="bi bi-envelope-exclamation-fill me-2"></i>
|
||||
<strong class="me-auto"><a :href="'#' + message.ID">New message</a></strong>
|
||||
<small class="text-body-secondary">now</small>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="toast-body">
|
||||
<div>
|
||||
<a :href="'#' + message.ID" class="d-block text-truncate text-muted">
|
||||
<template v-if="message.Subject != ''">{{ message.Subject }}</template>
|
||||
<template v-else>[ no subject ]</template>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
28
server/ui/api/v1/index.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Mailpit API v1 documentation</title>
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<link rel="icon" href="../../favicon.svg">
|
||||
<script src="../../dist/docs.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<rapi-doc id="thedoc" spec-url="swagger.json" theme="light" layout="column" render-style="read" load-fonts="false"
|
||||
regular-font="system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'"
|
||||
mono-font="Courier New, Courier, System, fixed-width" font-size="large" allow-spec-url-load="false"
|
||||
allow-spec-file-load="false" allow-server-selection="false" allow-search="false" allow-advanced-search="false"
|
||||
show-curl-before-try="true" bg-color="#ffffff" nav-bg-color="#e3e8ec" nav-text-color="#212529"
|
||||
nav-hover-bg-color="#fff" header-color="#2c3e50" primary-color="#2c3e50" text-color="#212529">
|
||||
<div slot="header">Mailpit API v1 documentation</div>
|
||||
<a target='_blank' href="../../" slot="logo">
|
||||
<img src="../../mailpit.svg" width="40" alt="Mailpit" />
|
||||
</a>
|
||||
</rapi-doc>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
957
server/ui/api/v1/swagger.json
Normal file
@@ -0,0 +1,957 @@
|
||||
{
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http"
|
||||
],
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "OpenAPI 2.0 documentation for [Mailpit](https://github.com/axllent/mailpit).",
|
||||
"title": "Mailpit API",
|
||||
"contact": {
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/axllent/mailpit"
|
||||
},
|
||||
"license": {
|
||||
"name": "MIT license",
|
||||
"url": "https://github.com/axllent/mailpit/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "v1"
|
||||
},
|
||||
"paths": {
|
||||
"/api/v1/info": {
|
||||
"get": {
|
||||
"description": "Returns basic runtime information, message totals and latest release version.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"application"
|
||||
],
|
||||
"summary": "Get application information",
|
||||
"operationId": "AppInformation",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/InfoResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/message/{ID}": {
|
||||
"get": {
|
||||
"description": "Returns the summary of a message, marking the message as read.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Get message summary",
|
||||
"operationId": "Message",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Message",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Message"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/message/{ID}/headers": {
|
||||
"get": {
|
||||
"description": "Returns the message headers as an array.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Get message headers",
|
||||
"operationId": "Headers",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "MessageHeaders",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MessageHeaders"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/message/{ID}/part/{PartID}": {
|
||||
"get": {
|
||||
"description": "This will return the attachment part using the appropriate Content-Type.",
|
||||
"produces": [
|
||||
"application/*",
|
||||
"image/*",
|
||||
"text/*"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Get message attachment",
|
||||
"operationId": "Attachment",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "attachment part id",
|
||||
"name": "PartID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/BinaryResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/message/{ID}/part/{PartID}/thumb": {
|
||||
"get": {
|
||||
"description": "This will return a cropped 180x120 JPEG thumbnail of an image attachment.\nIf the image is smaller than 180x120 then the image is padded. If the attachment is not an image then a blank image is returned.",
|
||||
"produces": [
|
||||
"image/jpeg"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Get an attachment image thumbnail",
|
||||
"operationId": "Thumbnail",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "attachment part id",
|
||||
"name": "PartID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/BinaryResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/message/{ID}/raw": {
|
||||
"get": {
|
||||
"description": "Returns the full email source as plain text.",
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Get message source",
|
||||
"operationId": "Raw",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/TextResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/message/{ID}/release": {
|
||||
"post": {
|
||||
"description": "Release a message via a preconfigured external SMTP server..",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Release message",
|
||||
"operationId": "Release",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Array of email addresses to release message to",
|
||||
"name": "To",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"description": "Array of email addresses to release message to",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/ReleaseMessageRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/OKResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/messages": {
|
||||
"get": {
|
||||
"description": "Returns messages from the mailbox ordered from newest to oldest.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"messages"
|
||||
],
|
||||
"summary": "List messages",
|
||||
"operationId": "GetMessages",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"description": "pagination offset",
|
||||
"name": "start",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
"description": "limit results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/MessagesSummaryResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "If no IDs are provided then all messages are updated.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"messages"
|
||||
],
|
||||
"summary": "Set read status",
|
||||
"operationId": "SetReadStatus",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Message ids to update",
|
||||
"name": "ids",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"description": "Message ids to update",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/SetReadStatusRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/OKResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "If no IDs are provided then all messages are deleted.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"messages"
|
||||
],
|
||||
"summary": "Delete messages",
|
||||
"operationId": "Delete",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Message ids to delete",
|
||||
"name": "ids",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"description": "Message ids to delete",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/DeleteRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/OKResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/search": {
|
||||
"get": {
|
||||
"description": "Returns the latest messages matching a search.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"messages"
|
||||
],
|
||||
"summary": "Search messages",
|
||||
"operationId": "MessagesSummary",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "search query",
|
||||
"name": "query",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 50,
|
||||
"description": "limit results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/MessagesSummaryResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/tags": {
|
||||
"put": {
|
||||
"description": "To remove all tags from a message, pass an empty tags array.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"tags"
|
||||
],
|
||||
"summary": "Set message tags",
|
||||
"operationId": "SetTags",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Message ids to update",
|
||||
"name": "ids",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"description": "Message ids to update",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/SetTagsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/OKResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/webui": {
|
||||
"get": {
|
||||
"description": "Returns configuration settings for the web UI.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"application"
|
||||
],
|
||||
"summary": "Get web UI configuration",
|
||||
"operationId": "WebUIConfiguration",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/WebUIConfigurationResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"Address": {
|
||||
"description": "An address such as \"Barry Gibbs \u003cbg@example.com\u003e\" is represented\nas Address{Name: \"Barry Gibbs\", Address: \"bg@example.com\"}.",
|
||||
"type": "object",
|
||||
"title": "Address represents a single mail address.",
|
||||
"properties": {
|
||||
"Address": {
|
||||
"type": "string"
|
||||
},
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-go-package": "net/mail"
|
||||
},
|
||||
"AppInformation": {
|
||||
"description": "Response includes the current and latest Mailpit version, database info, and memory usage",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Database": {
|
||||
"description": "Database path",
|
||||
"type": "string"
|
||||
},
|
||||
"DatabaseSize": {
|
||||
"description": "Database size in bytes",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"LatestVersion": {
|
||||
"description": "Latest Mailpit version",
|
||||
"type": "string"
|
||||
},
|
||||
"Memory": {
|
||||
"description": "Current memory usage in bytes",
|
||||
"type": "integer",
|
||||
"format": "uint64"
|
||||
},
|
||||
"Messages": {
|
||||
"description": "Total number of messages in the database",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"Version": {
|
||||
"description": "Current Mailpit version",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-go-name": "appInformation",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"Attachment": {
|
||||
"description": "Attachment struct for inline and attachments",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ContentID": {
|
||||
"description": "content id",
|
||||
"type": "string"
|
||||
},
|
||||
"ContentType": {
|
||||
"description": "content type",
|
||||
"type": "string"
|
||||
},
|
||||
"FileName": {
|
||||
"description": "file name",
|
||||
"type": "string"
|
||||
},
|
||||
"PartID": {
|
||||
"description": "attachment part id",
|
||||
"type": "string"
|
||||
},
|
||||
"Size": {
|
||||
"description": "size in bytes",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"x-go-package": "github.com/axllent/mailpit/storage"
|
||||
},
|
||||
"DeleteRequest": {
|
||||
"description": "Delete request",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"description": "ids\nin:body",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "IDs"
|
||||
}
|
||||
},
|
||||
"x-go-name": "deleteRequest",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"Message": {
|
||||
"description": "Message data excluding physical attachments",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Attachments": {
|
||||
"description": "Message attachments",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Attachment"
|
||||
}
|
||||
},
|
||||
"Bcc": {
|
||||
"description": "Bcc addresses",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Address"
|
||||
}
|
||||
},
|
||||
"Cc": {
|
||||
"description": "Cc addresses",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Address"
|
||||
}
|
||||
},
|
||||
"Date": {
|
||||
"description": "Message date if set, else date received",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"From": {
|
||||
"$ref": "#/definitions/Address"
|
||||
},
|
||||
"HTML": {
|
||||
"description": "Message body HTML",
|
||||
"type": "string"
|
||||
},
|
||||
"ID": {
|
||||
"description": "Unique message database id",
|
||||
"type": "string"
|
||||
},
|
||||
"Inline": {
|
||||
"description": "Inline message attachments",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Attachment"
|
||||
}
|
||||
},
|
||||
"Read": {
|
||||
"description": "Read status",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ReplyTo": {
|
||||
"description": "ReplyTo addresses",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Address"
|
||||
}
|
||||
},
|
||||
"ReturnPath": {
|
||||
"description": "ReturnPath is the Return-Path",
|
||||
"type": "string"
|
||||
},
|
||||
"Size": {
|
||||
"description": "Message size in bytes",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"Subject": {
|
||||
"description": "Message subject",
|
||||
"type": "string"
|
||||
},
|
||||
"Tags": {
|
||||
"description": "Message tags",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Text": {
|
||||
"description": "Message body text",
|
||||
"type": "string"
|
||||
},
|
||||
"To": {
|
||||
"description": "To addresses",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Address"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-go-package": "github.com/axllent/mailpit/storage"
|
||||
},
|
||||
"MessageHeaders": {
|
||||
"description": "Message headers",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-go-name": "messageHeaders",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"MessageSummary": {
|
||||
"description": "MessageSummary struct for frontend messages",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Attachments": {
|
||||
"description": "Whether the message has any attachments",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"Bcc": {
|
||||
"description": "Bcc addresses",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Address"
|
||||
}
|
||||
},
|
||||
"Cc": {
|
||||
"description": "Cc addresses",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Address"
|
||||
}
|
||||
},
|
||||
"Created": {
|
||||
"description": "Created time",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"From": {
|
||||
"$ref": "#/definitions/Address"
|
||||
},
|
||||
"ID": {
|
||||
"description": "Unique message database id",
|
||||
"type": "string"
|
||||
},
|
||||
"Read": {
|
||||
"description": "Read status",
|
||||
"type": "boolean"
|
||||
},
|
||||
"Size": {
|
||||
"description": "Message size in bytes (total)",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"Subject": {
|
||||
"description": "Email subject",
|
||||
"type": "string"
|
||||
},
|
||||
"Tags": {
|
||||
"description": "Message tags",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"To": {
|
||||
"description": "To address",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Address"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-go-package": "github.com/axllent/mailpit/storage"
|
||||
},
|
||||
"MessagesSummary": {
|
||||
"description": "MessagesSummary is a summary of a list of messages",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"description": "Number of results returned",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Count"
|
||||
},
|
||||
"messages": {
|
||||
"description": "Messages summary\nin:body",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/MessageSummary"
|
||||
},
|
||||
"x-go-name": "Messages"
|
||||
},
|
||||
"start": {
|
||||
"description": "Pagination offset",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Start"
|
||||
},
|
||||
"tags": {
|
||||
"description": "All current tags",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "Tags"
|
||||
},
|
||||
"total": {
|
||||
"description": "Total number of messages in mailbox",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Total"
|
||||
},
|
||||
"unread": {
|
||||
"description": "Total number of unread messages in mailbox",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Unread"
|
||||
}
|
||||
},
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"ReleaseMessageRequest": {
|
||||
"description": "Release request",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to": {
|
||||
"description": "To\nin:body",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "To"
|
||||
}
|
||||
},
|
||||
"x-go-name": "releaseMessageRequest",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"SetReadStatusRequest": {
|
||||
"description": "Set read status request",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"description": "ids\nin:body",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "IDs"
|
||||
},
|
||||
"read": {
|
||||
"description": "Read status",
|
||||
"type": "boolean",
|
||||
"x-go-name": "Read"
|
||||
}
|
||||
},
|
||||
"x-go-name": "setReadStatusRequest",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"SetTagsRequest": {
|
||||
"description": "Set tags request",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"description": "IDs\nin:body",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "IDs"
|
||||
},
|
||||
"tags": {
|
||||
"description": "Tags\nin:body",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "Tags"
|
||||
}
|
||||
},
|
||||
"x-go-name": "setTagsRequest",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"WebUIConfiguration": {
|
||||
"description": "Response includes global web UI settings",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"MessageRelay": {
|
||||
"description": "Message Relay information",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Enabled": {
|
||||
"description": "Whether message relaying (release) is enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ReturnPath": {
|
||||
"description": "Enforced Return-Path (if set) for relay bounces",
|
||||
"type": "string"
|
||||
},
|
||||
"SMTPServer": {
|
||||
"description": "The configured SMTP server address",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-go-name": "webUIConfiguration",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"BinaryResponse": {
|
||||
"description": "Binary data reponse inherits the attachment's content type"
|
||||
},
|
||||
"ErrorResponse": {
|
||||
"description": "Error reponse"
|
||||
},
|
||||
"InfoResponse": {
|
||||
"description": "Application information",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/AppInformation"
|
||||
},
|
||||
"headers": {
|
||||
"Body": {
|
||||
"description": "Application information"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessagesSummaryResponse": {
|
||||
"description": "Message summary",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MessagesSummary"
|
||||
}
|
||||
},
|
||||
"OKResponse": {
|
||||
"description": "Plain text \"ok\" reponse"
|
||||
},
|
||||
"TextResponse": {
|
||||
"description": "Plain text response"
|
||||
},
|
||||
"WebUIConfigurationResponse": {
|
||||
"description": "Web UI configuration",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/WebUIConfiguration"
|
||||
},
|
||||
"headers": {
|
||||
"Body": {
|
||||
"description": "Web UI configuration settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
server/ui/favicon.svg
Normal 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 |
@@ -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>
|
||||
|
||||
@@ -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 |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,25 +404,63 @@ func GetMessage(id string) (*Message, error) {
|
||||
from = &mail.Address{Name: env.GetHeader("From")}
|
||||
}
|
||||
|
||||
date, _ := env.Date()
|
||||
returnPath := strings.Trim(env.GetHeader("Return-Path"), "<>")
|
||||
if returnPath == "" {
|
||||
returnPath = from.Address
|
||||
}
|
||||
|
||||
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,
|
||||
Read: true,
|
||||
From: from,
|
||||
Date: date,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Size: len(raw),
|
||||
Text: env.Text,
|
||||
ID: id,
|
||||
Read: true,
|
||||
From: from,
|
||||
Date: date,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
ReplyTo: addressToSlice(env, "Reply-To"),
|
||||
ReturnPath: returnPath,
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Tags: getMessageTags(id),
|
||||
Size: len(raw),
|
||||
Text: env.Text,
|
||||
}
|
||||
|
||||
// strip base tags
|
||||
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 +717,8 @@ func DeleteAllMessages() error {
|
||||
dbLastAction = time.Now()
|
||||
dbDataDeleted = false
|
||||
|
||||
websockets.Broadcast("prune", nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -655,9 +731,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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
@@ -230,7 +231,7 @@ func BenchmarkImportMime(b *testing.B) {
|
||||
}
|
||||
|
||||
func setup() {
|
||||
config.NoLogging = true
|
||||
logger.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -7,43 +7,85 @@ import (
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
// Message struct for loading messages. It does not include physical attachments.
|
||||
// Message data excluding physical attachments
|
||||
//
|
||||
// swagger:model Message
|
||||
type Message struct {
|
||||
ID string
|
||||
Read bool
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
Subject string
|
||||
Date time.Time
|
||||
Text string
|
||||
HTML string
|
||||
Size int
|
||||
Inline []Attachment
|
||||
// Unique message database id
|
||||
ID string
|
||||
// Read status
|
||||
Read bool
|
||||
// From address
|
||||
From *mail.Address
|
||||
// To addresses
|
||||
To []*mail.Address
|
||||
// Cc addresses
|
||||
Cc []*mail.Address
|
||||
// Bcc addresses
|
||||
Bcc []*mail.Address
|
||||
// ReplyTo addresses
|
||||
ReplyTo []*mail.Address
|
||||
// ReturnPath is the Return-Path
|
||||
ReturnPath string
|
||||
// Message subject
|
||||
Subject string
|
||||
// Message date if set, else date received
|
||||
Date time.Time
|
||||
// Message tags
|
||||
Tags []string
|
||||
// Message body text
|
||||
Text string
|
||||
// Message body HTML
|
||||
HTML string
|
||||
// Message size in bytes
|
||||
Size int
|
||||
// Inline message attachments
|
||||
Inline []Attachment
|
||||
// Message attachments
|
||||
Attachments []Attachment
|
||||
}
|
||||
|
||||
// Attachment struct for inline and attachments
|
||||
//
|
||||
// swagger:model Attachment
|
||||
type Attachment struct {
|
||||
PartID string
|
||||
FileName string
|
||||
// attachment part id
|
||||
PartID string
|
||||
// file name
|
||||
FileName string
|
||||
// content type
|
||||
ContentType string
|
||||
ContentID string
|
||||
Size int
|
||||
// content id
|
||||
ContentID string
|
||||
// size in bytes
|
||||
Size int
|
||||
}
|
||||
|
||||
// Summary struct for frontend messages
|
||||
type Summary struct {
|
||||
ID string
|
||||
Read bool
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
Subject string
|
||||
Created time.Time
|
||||
Size int
|
||||
// MessageSummary struct for frontend messages
|
||||
//
|
||||
// swagger:model MessageSummary
|
||||
type MessageSummary struct {
|
||||
// Unique message database id
|
||||
ID string
|
||||
// Read status
|
||||
Read bool
|
||||
// From address
|
||||
From *mail.Address
|
||||
// To address
|
||||
To []*mail.Address
|
||||
// Cc addresses
|
||||
Cc []*mail.Address
|
||||
// Bcc addresses
|
||||
Bcc []*mail.Address
|
||||
// Email subject
|
||||
Subject string
|
||||
// Created time
|
||||
Created time.Time
|
||||
// Message tags
|
||||
Tags []string
|
||||
// Message size in bytes (total)
|
||||
Size int
|
||||
// Whether the message has any attachments
|
||||
Attachments int
|
||||
}
|
||||
|
||||
@@ -51,6 +93,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
@@ -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
|
||||
}
|
||||
@@ -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, "%", "%%")
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
// Package logger handles the logging
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
log *logrus.Logger
|
||||
// VerboseLogging for verbose logging
|
||||
VerboseLogging bool
|
||||
// QuietLogging shows only errors
|
||||
QuietLogging bool
|
||||
// NoLogging shows only fatal errors
|
||||
NoLogging bool
|
||||
)
|
||||
|
||||
// Log returns the logger instance
|
||||
@@ -18,13 +25,13 @@ func Log() *logrus.Logger {
|
||||
if log == nil {
|
||||
log = logrus.New()
|
||||
log.SetLevel(logrus.InfoLevel)
|
||||
if config.VerboseLogging {
|
||||
if VerboseLogging {
|
||||
// verbose logging (debug)
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
} else if config.QuietLogging {
|
||||
} else if QuietLogging {
|
||||
// show errors only
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
} else if config.NoLogging {
|
||||
} else if NoLogging {
|
||||
// disable all logging (tests)
|
||||
log.SetLevel(logrus.PanicLevel)
|
||||
}
|
||||
@@ -45,3 +52,14 @@ func PrettyPrint(i interface{}) {
|
||||
s, _ := json.MarshalIndent(i, "", "\t")
|
||||
fmt.Println(string(s))
|
||||
}
|
||||
|
||||
// CleanIP returns a human-readable IP for the logging interface
|
||||
// when starting services. It translates [::]:<port> to "localhost:<port>"
|
||||
func CleanIP(s string) string {
|
||||
re := regexp.MustCompile(`^\[\:\:\]\:\d+`)
|
||||
if re.MatchString(s) {
|
||||
return "0.0.0.0:" + s[5:]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
59
utils/tools/message.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Package tools provides various methods for variouws things
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
)
|
||||
|
||||
// RemoveMessageHeaders scans a message for headers, if found them removes them.
|
||||
// It will only remove a single instance of any header, and is intended to remove
|
||||
// Bcc & Message-Id.
|
||||
func RemoveMessageHeaders(msg []byte, headers []string) ([]byte, error) {
|
||||
reader := bytes.NewReader(msg)
|
||||
m, err := mail.ReadMessage(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reBlank := regexp.MustCompile(`^\s+`)
|
||||
|
||||
for _, hdr := range headers {
|
||||
// case-insentitive
|
||||
reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta(hdr+":"))
|
||||
|
||||
// header := []byte(hdr + ":")
|
||||
if m.Header.Get(hdr) != "" {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(msg))
|
||||
found := false
|
||||
hdr := []byte("")
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
if !found && reHdr.Match(line) {
|
||||
// add the first line starting with <header>:
|
||||
hdr = append(hdr, line...)
|
||||
hdr = append(hdr, []byte("\r\n")...)
|
||||
found = true
|
||||
} else if found && reBlank.Match(line) {
|
||||
// add any following lines starting with a whitespace (tab or space)
|
||||
hdr = append(hdr, line...)
|
||||
hdr = append(hdr, []byte("\r\n")...)
|
||||
} else if found {
|
||||
// stop scanning, we have the full <header>
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(hdr) > 0 {
|
||||
logger.Log().Debugf("[release] removing %s header", hdr)
|
||||
msg = bytes.Replace(msg, hdr, []byte(""), 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/axllent/mailpit/logger"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/axllent/semver"
|
||||
)
|
||||
|
||||