Showing preview only (1,710K chars total). Download the full file or copy to clipboard to get everything.
Repository: axllent/mailpit
Branch: develop
Commit: 7b22d6a5f9ff
Files: 185
Total size: 1.6 MB
Directory structure:
gitextract_6s7dfcpy/
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ ├── SECURITY.md
│ ├── cliff.toml
│ ├── dependabot.yml
│ └── workflows/
│ ├── build-docker-edge.yml
│ ├── build-docker.yml
│ ├── close-stale-issues.yml
│ ├── codeql-analysis.yml
│ ├── release-build.yml
│ ├── tests-rqlite.yml
│ └── tests.yml
├── .gitignore
├── .prettierignore
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── cmd/
│ ├── dump.go
│ ├── ingest.go
│ ├── readyz.go
│ ├── reindex.go
│ ├── root.go
│ ├── sendmail.go
│ └── version.go
├── config/
│ ├── config.go
│ ├── tags.go
│ ├── utils.go
│ └── validators.go
├── esbuild.config.mjs
├── eslint.config.js
├── go.mod
├── go.sum
├── install.sh
├── internal/
│ ├── auth/
│ │ └── auth.go
│ ├── dump/
│ │ └── dump.go
│ ├── html2text/
│ │ ├── html2text.go
│ │ └── html2text_test.go
│ ├── htmlcheck/
│ │ ├── README.md
│ │ ├── caniemail-data.json
│ │ ├── caniemail.go
│ │ ├── config.go
│ │ ├── css.go
│ │ ├── html.go
│ │ ├── inline_test.go
│ │ ├── main.go
│ │ ├── platforms.go
│ │ └── structs.go
│ ├── linkcheck/
│ │ ├── linkcheck_test.go
│ │ ├── main.go
│ │ ├── status.go
│ │ └── structs.go
│ ├── logger/
│ │ └── logger.go
│ ├── pop3/
│ │ ├── functions.go
│ │ ├── pop3_test.go
│ │ └── server.go
│ ├── pop3client/
│ │ └── client.go
│ ├── prometheus/
│ │ └── metrics.go
│ ├── smtpd/
│ │ ├── chaos/
│ │ │ └── chaos.go
│ │ ├── forward.go
│ │ ├── main.go
│ │ ├── relay.go
│ │ ├── smtpd.go
│ │ └── smtpd_test.go
│ ├── snakeoil/
│ │ └── snakeoil.go
│ ├── spamassassin/
│ │ ├── postmark/
│ │ │ └── postmark.go
│ │ ├── spamassassin.go
│ │ └── spamc/
│ │ └── spamc.go
│ ├── stats/
│ │ └── stats.go
│ ├── storage/
│ │ ├── cron.go
│ │ ├── database.go
│ │ ├── functions_test.go
│ │ ├── messages.go
│ │ ├── messages_test.go
│ │ ├── notifications.go
│ │ ├── reindex.go
│ │ ├── schemas/
│ │ │ ├── 1.0.0.sql
│ │ │ ├── 1.1.0.sql
│ │ │ ├── 1.2.0.sql
│ │ │ ├── 1.21.2.sql
│ │ │ ├── 1.21.8.sql
│ │ │ ├── 1.23.0.sql
│ │ │ ├── 1.3.0.sql
│ │ │ ├── 1.4.0.sql
│ │ │ ├── 1.5.0.sql
│ │ │ └── README.md
│ │ ├── schemas.go
│ │ ├── search.go
│ │ ├── search_test.go
│ │ ├── settings.go
│ │ ├── structs.go
│ │ ├── tagfilters.go
│ │ ├── tags.go
│ │ ├── tags_test.go
│ │ ├── testdata/
│ │ │ ├── inline-attachment.eml
│ │ │ ├── mime-attachment.eml
│ │ │ ├── mixed-attachment.eml
│ │ │ ├── plain-text.eml
│ │ │ ├── regular-attachment.eml
│ │ │ └── tags.eml
│ │ └── utils.go
│ └── tools/
│ ├── argsparser.go
│ ├── fs.go
│ ├── headers.go
│ ├── html.go
│ ├── listunsubscribeparser.go
│ ├── net.go
│ ├── snippets.go
│ ├── tags.go
│ ├── tools_test.go
│ ├── unixsocket.go
│ └── utils.go
├── main.go
├── package.json
├── sendmail/
│ ├── cmd/
│ │ ├── cmd.go
│ │ └── smtp.go
│ └── main.go
└── server/
├── apiv1/
│ ├── api.go
│ ├── application.go
│ ├── chaos.go
│ ├── message.go
│ ├── messages.go
│ ├── other.go
│ ├── release.go
│ ├── send.go
│ ├── structs.go
│ ├── swagger-config.yml
│ ├── swaggerParams.go
│ ├── swaggerResponses.go
│ ├── tags.go
│ ├── testing.go
│ └── thumbnails.go
├── cors.go
├── cors_test.go
├── embed.go
├── handlers/
│ ├── k8healthz.go
│ ├── k8sready.go
│ ├── messages.go
│ └── proxy.go
├── server.go
├── server_test.go
├── ui/
│ └── api/
│ └── v1/
│ ├── index.html
│ └── swagger.json
├── ui-src/
│ ├── App.vue
│ ├── app.js
│ ├── assets/
│ │ ├── _bootstrap.scss
│ │ ├── _bootstrap_variables.scss
│ │ └── styles.scss
│ ├── components/
│ │ ├── AjaxLoader.vue
│ │ ├── AppAbout.vue
│ │ ├── AppBadge.vue
│ │ ├── AppFavicon.vue
│ │ ├── AppNotifications.vue
│ │ ├── AppSettings.vue
│ │ ├── EditTags.vue
│ │ ├── ListMessages.vue
│ │ ├── NavMailbox.vue
│ │ ├── NavPagination.vue
│ │ ├── NavSearch.vue
│ │ ├── NavSelected.vue
│ │ ├── NavTags.vue
│ │ ├── SearchForm.vue
│ │ └── message/
│ │ ├── HTMLCheck.vue
│ │ ├── LinkCheck.vue
│ │ ├── MessageAttachments.vue
│ │ ├── MessageHeaders.vue
│ │ ├── MessageItem.vue
│ │ ├── MessageRelease.vue
│ │ ├── MessageScreenshot.vue
│ │ └── SpamAssassin.vue
│ ├── docs.js
│ ├── mixins/
│ │ ├── CommonMixins.js
│ │ └── MessagesMixins.js
│ ├── router/
│ │ └── index.js
│ ├── stores/
│ │ ├── mailbox.js
│ │ └── pagination.js
│ └── views/
│ ├── MailboxView.vue
│ ├── MessageView.vue
│ ├── NotFoundView.vue
│ └── SearchView.vue
├── webhook/
│ └── webhook.go
└── websockets/
├── client.go
└── hub.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
/node_modules
/mailpit
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [axllent]
thanks_dev: u/gh/axllent
================================================
FILE: .github/SECURITY.md
================================================
# Reporting security vulnerabilities
Your efforts to responsibly disclose your findings are appreciated.
**Please do not report security vulnerabilities through public GitHub issues.**
If you believe you have found a security vulnerability, you can report it using one of the following methods:
1. **GitHub Security Advisory (Recommended):** Use the "Report a vulnerability" button in the [Security tab](../../security/advisories/new) of this repository.
2. **Email:** Send your findings to security@axllent.org
Your report should include:
- Mailpit version
- A vulnerability description
- Reproduction steps (if applicable)
- Any other details you think are likely to be important
You should receive an initial acknowledgement within 24 hours in most cases, and will be kept updated throughout the process.
With your consent, your contributions will be publicly acknowledged.
================================================
FILE: .github/cliff.toml
================================================
## https://git-cliff.org/
[changelog]
body = """
{% if version %}\
\n## [{{ version }}]
{% else %}\
\n## Unreleased
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}\
{% for commit in commits %}
- {{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
footer = ""
header = "# Changelog\n\nNotable changes to Mailpit will be documented in this file."
postprocessors = [
{pattern = "reponse", replace = "response"},
{pattern = "messsage", replace = "message"},
{pattern = '(?i) go modules', replace = " Go dependencies"},
{pattern = '(?i) node modules', replace = " node dependencies"},
{pattern = '#([0-9]+)', replace = "[#$1](https://github.com/axllent/mailpit/issues/$1)"},
]
trim = true
[git]
# HTML comments added for grouping order, stripped on generation
commit_parsers = [
{body = ".*security", group = "<!-- 1 -->Security"},
{message = "(?i)^security", group = "<!-- 1 -->Security"},
{message = "(?i)^feat", group = "<!-- 2 -->Feature"},
{message = "(?i)^chore", group = "<!-- 3 -->Chore"},
{message = "(?i)^libs", group = "<!-- 3 -->Chore"},
{message = "(?i)^ui", group = "<!-- 3 -->Chore"},
{message = "(?i)^api", group = "<!-- 4 -->API"},
{message = "(?i)^fix", group = "<!-- 5 -->Fix"},
{message = "(?i)^doc", group = "<!-- 6 -->Documentation", default_scope = "unscoped"},
{message = "(?i)^swagger", group = "<!-- 6 -->Documentation", default_scope = "unscoped"},
{message = "(?i)^test", group = "<!-- 7 -->Test"},
]
# Exclude commits that are not matched by any commit parser.
# filter_commits = true
# Order releases topologically instead of chronologically.
# topo_order = true
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "oldest"
================================================
FILE: .github/dependabot.yml
================================================
# 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: "quarterly"
- package-ecosystem: "github-actions"
directory: "/" # Location of package manifests
schedule:
interval: "quarterly"
- package-ecosystem: "docker"
directory: "/" # Location of package manifests
schedule:
interval: "quarterly"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "quarterly"
================================================
FILE: .github/workflows/build-docker-edge.yml
================================================
on:
push:
branches: [ develop ]
name: Build docker edge images
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0 # required for github-action-get-previous-tag
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Log into GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Get previous git tag
uses: WyriHaximus/github-action-get-previous-tag@v2
id: previous-tag
- name: Get short SHA
uses: benjlevesque/short-sha@v3.0
id: short-sha
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/386,linux/amd64,linux/arm64
build-args: |
"VERSION=${{ steps.previous-tag.outputs.tag }}-${{ steps.short-sha.outputs.sha }}"
push: true
tags: |
axllent/mailpit:edge
ghcr.io/${{ github.repository }}:edge
================================================
FILE: .github/workflows/build-docker.yml
================================================
on:
release:
types: [created]
name: Build docker images
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Log into GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.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@v6
with:
context: .
platforms: linux/386,linux/amd64,linux/arm64
build-args: |
"VERSION=${{ github.ref_name }}"
push: true
tags: |
axllent/mailpit:latest
axllent/mailpit:${{ github.ref_name }}
axllent/mailpit:v${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:v${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}
ghcr.io/${{ github.repository }}:latest
================================================
FILE: .github/workflows/close-stale-issues.yml
================================================
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@v10.1.1
with:
days-before-issue-stale: 7
days-before-issue-close: 3
exempt-issue-labels: "enhancement,bug,awaiting feedback"
stale-issue-label: "stale"
close-issue-reason: "completed"
stale-issue-message: "This issue has been marked as stale because it has been open for 7 days with no activity."
close-issue-message: "This issue was closed because there has been no activity since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "develop" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "develop" ]
schedule:
- cron: '34 23 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
================================================
FILE: .github/workflows/release-build.yml
================================================
on:
release:
types: [created]
name: Build & release
jobs:
releases-matrix:
name: Build
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, windows, darwin]
goarch: ["386", amd64, arm, arm64]
exclude:
- goarch: "386"
goos: darwin
- goarch: "386"
goos: windows
- goarch: arm
goos: darwin
- goarch: arm
goos: windows
steps:
- uses: actions/checkout@v6
# build the assets
- uses: actions/setup-node@v6
with:
node-version: 22
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
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
binary_name: "mailpit"
pre_command: export CGO_ENABLED=0
asset_name: mailpit-${{ matrix.goos }}-${{ matrix.goarch }}
extra_files: LICENSE README.md
md5sum: false
overwrite: true
retry: 5
ldflags: -w -X "github.com/axllent/mailpit/config.Version=${{ github.ref_name }}"
================================================
FILE: .github/workflows/tests-rqlite.yml
================================================
name: Tests (rqlite)
on:
pull_request:
branches: [ develop, 'feature/**' ]
push:
branches: [ develop, 'feature/**' ]
jobs:
test-rqlite:
runs-on: ubuntu-latest
services:
rqlite:
image: rqlite/rqlite:latest
ports:
- 4001:4001
env:
# the HTTP address the rqlite node should advertise
HTTP_ADV_ADDR: "localhost:4001"
steps:
- uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: 'stable'
cache-dependency-path: "**/*.sum"
- run: go test -p 1 ./internal/storage ./server ./internal/smtpd ./internal/pop3 ./internal/tools ./internal/html2text ./internal/htmlcheck ./internal/linkcheck -v
env:
# set Mailpit to use the rqlite service container
MP_DATABASE: "http://localhost:4001"
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on:
pull_request:
branches: [ develop, 'feature/**' ]
push:
branches: [ develop, 'feature/**' ]
jobs:
test:
strategy:
matrix:
go-version: [stable]
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: false
- uses: actions/checkout@v6
- name: Set up Go environment
uses: actions/cache@v5
with:
path: |
~/.cache/go-build
~/go
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Test Go linting (gofmt)
if: startsWith(matrix.os, 'ubuntu') == true
# https://olegk.dev/github-actions-and-go
run: gofmt -s -w . && git diff --exit-code
- name: Run Go tests
run: go test -p 1 ./internal/storage ./server ./internal/smtpd ./internal/pop3 ./internal/tools ./internal/html2text ./internal/htmlcheck ./internal/linkcheck -v
- name: Run Go benchmarking
run: go test -p 1 ./internal/storage ./internal/html2text -bench=.
# build the assets
- name: Set up node environment
if: startsWith(matrix.os, 'ubuntu') == true
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'npm'
- name: Install JavaScript dependencies
if: startsWith(matrix.os, 'ubuntu') == true
run: npm install
- name: Run JavaScript linting
if: startsWith(matrix.os, 'ubuntu') == true
run: npm run lint
- name: Test JavaScript packaging
if: startsWith(matrix.os, 'ubuntu') == true
run: npm run package
# # validate the swagger file
# - name: Validate OpenAPI definition
# if: startsWith(matrix.os, 'ubuntu') == true
# uses: swaggerexpert/swagger-editor-validate@v1
# with:
# definition-file: server/ui/api/v1/swagger.json
# default-timeout: 20000
================================================
FILE: .gitignore
================================================
/node_modules/
/send
/sendmail/sendmail
/server/ui/dist
/Makefile
/mailpit*
/.idea
*.old
*.db
================================================
FILE: .prettierignore
================================================
# Not within the scope of Prettier
**/*.yml
**/*.yaml
**/*.json
**/*.md
**/*.css
**/*.html
**/*.scss
composer.lock
================================================
FILE: .vscode/settings.json
================================================
{
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"cSpell.words": [
"AUTHCRAMMD",
"AUTHLOGIN",
"AUTHPLAIN",
"bordercolor",
"CRAMMD",
"dateparse",
"EHLO",
"ESMTP",
"EXPN",
"gofmt",
"Healthz",
"HTTPIP",
"Inlines",
"jhillyerd",
"leporo",
"lithammer",
"livez",
"Mechs",
"navhtml",
"neostandard",
"nolint",
"popperjs",
"readyz",
"RSET",
"shortuuid",
"SMTPTLS",
"swaggerexpert",
"UITLS",
"VRFY",
"writef"
]
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
Notable changes to Mailpit will be documented in this file.
## [v1.29.3]
### Security
- Enhance CORS origin handling to respect host:port distinctions
- Limit proxy requests to 50MB to prevent OOM attacks
- Enhance HTML sanitization in message view
- Enhance HTML sanitization in screenshot generation
- Escape ContentID in HTML replacement to prevent regex injection
### Chore
- Use last release + git hash in Docker edge versions
- Bump minimatch from 10.2.2 to 10.2.4
- Refactor code with go fix
- Switch to math/rand/v2
- Refactor API send authentication logic
- Refactor events websocket middleware
- Set timeout for HTTP client in webhook Send function
- Use local hostname for EHLO/HELO in SMTP communication
- Simplify HTML decoding function in screenshot generation using DOMParser
- Set margin & padding to HTML screenshot to prevent transparent top/left border
- Replace localStorage retrieval with a dedicated function for default release addresses
- Limit subject length to 100 characters in browser notifications
- Improve transaction handling in pruneMessages and fix loop continuation in InitDB
- Update Content-Disposition header to use inline display and escape filename
- Refactor timezone handling in searchQueryBuilder
- Update Go dependencies
- Update node dependencies
### Fix
- Update SQL query to use tenant when using is:tagged filter
## [v1.29.2]
### Security
- Prevent Server-Side Request Forgery (SSRF) via Link Check API ([GHSA-mpf7-p9x7-96r3](https://github.com/axllent/mailpit/security/advisories/GHSA-mpf7-p9x7-96r3))
### Chore
- Upgrade eslint JavaScript linting
- Update Go dependencies
- Update node dependencies
- Update caniemail test database
### Fix
- Update install instructions when setting INSTALL_PATH
- Include 8BITMIME in SMTPD EHLO response ([#648](https://github.com/axllent/mailpit/issues/648))
## [v1.29.1]
### Chore
- Add CORS error logging and update error messages for failed CORS requests
- Bump axios from 1.13.4 to 1.13.5
- Update Go dependencies
- Update node dependencies
### Fix
- Enable "Mark all read" button (Inbox) when new message is received
## [v1.29.0]
### Feature
- Include message attachment checksums (MD5, SHA1 & SHA254) in API message summary
- Option to display/hide attachment information in message view in web UI including checksums, content type & disposition
### Chore
- Add support for multi-origin CORS settings and apply to events websocket ([#630](https://github.com/axllent/mailpit/issues/630))
- Add support for webhook delay ([#627](https://github.com/axllent/mailpit/issues/627))
- Update Go dependencies
- Update node dependencies
### Test
- Add CORS tests
- Add message summary attachment checksum tests
## [v1.28.4]
### Chore
- Increase allowed SMTP email address length to 1024 chars & return clearer SMTP responses for failures ([#620](https://github.com/axllent/mailpit/issues/620))
- Update Go dependencies
- Update node dependencies
### Fix
- Ensure SMTP HELO/EHLO command is issued before MAIL FROM as per RFC 5321 ([#621](https://github.com/axllent/mailpit/issues/621))
- Prevent nested MAIL command during an active SMTP transaction ([#623](https://github.com/axllent/mailpit/issues/623))
- Avoid error on image type assertion in thumbnail generation
## [v1.28.3]
### Security
- Ensure SMTP TO & FROM addresses are RFC 5322 compliant and prevent header injection ([GHSA-54wq-72mp-cq7c](https://github.com/axllent/mailpit/security/advisories/GHSA-54wq-72mp-cq7c))
- Prevent Server-Side Request Forgery (SSRF) via HTML Check API ([GHSA-6jxm-fv7w-rw5j](https://github.com/axllent/mailpit/security/advisories/GHSA-6jxm-fv7w-rw5j))
### Chore
- Fix formatting and update reporting instructions in SECURITY.md ([#614](https://github.com/axllent/mailpit/issues/614))
- Allow `@` character in message tags & set max length to 100 characters per tag
- Update Go dependencies
- Update node dependencies
### Fix
- Correctly render default addresses in release modal after settings change ([#594](https://github.com/axllent/mailpit/issues/594))
- Correctly detect macOS group in install.sh ([#619](https://github.com/axllent/mailpit/issues/619))
- Auto-tagging using SMTP username using plain auth ([#617](https://github.com/axllent/mailpit/issues/617))
- Validate maximum lengths of email addresses - RFC5321 (section 4.5.3.1)
### Test
- Update tag tests with length limits and `@` character
- Add SMTP tests for address compliancy (RFC 5322) and header injection
- Add maximum email length validation tests - RFC5321 (section 4.5.3.1)
## [v1.28.2]
### Security
- Prevent Cross-Site WebSocket Hijacking (CSWSH) allowing unauthenticated access to message data [CVE-2026-22689](https://github.com/axllent/mailpit/security/advisories/GHSA-524m-q5m7-79mm)
### Feature
- Allow default mail addresses to be set when releasing message ([#594](https://github.com/axllent/mailpit/issues/594))
### Chore
- Remove webkit warnings about missing template / render functions
- Avoid empty URL query parameter when returning to inbox from message view
## [v1.28.1]
### Security
- Restrict screenshot proxy to only support asset links contained in messages [CVE-2026-21859](https://github.com/axllent/mailpit/security/advisories/GHSA-8v65-47jx-7mfr)
### Chore
- Bump actions/checkout from 5 to 6 ([#610](https://github.com/axllent/mailpit/issues/610))
- Bump actions/cache from 4 to 5 ([#607](https://github.com/axllent/mailpit/issues/607))
- Bump actions/stale from 10.0.0 to 10.1.1 ([#604](https://github.com/axllent/mailpit/issues/604))
- Bump actions/setup-node from 5 to 6 ([#598](https://github.com/axllent/mailpit/issues/598))
- Bump esbuild from 0.25.12 to 0.27.2 ([#611](https://github.com/axllent/mailpit/issues/611))
- Update Go dependencies
- Update node dependencies
### Test
- Add inline message tests
- Increase swagger test timeout
## [v1.28.0]
### Feature
- Optionally propagate SMTP errors ([#588](https://github.com/axllent/mailpit/issues/588))
### Chore
- Update Go dependencies
- Update node dependencies
- Update caniemail test database
## [v1.27.11]
### Chore
- Update Go dependencies
- Update node dependencies
- Add type assertion for value in imaging assignment
## [v1.27.10]
### Security
- Prevent potential information disclosure via indirect expvar library (Prometheus)
### Chore
- Add tooltip to messages nav dropdown
- Update GitHub Actions
- Add tooltip to messages nav dropdown
- Update GitHub Actions
- Update Go dependencies
- Update node dependencies
## [v1.27.9]
### Chore
- UI tweaks to pagination layout for clearer navigation ([#568](https://github.com/axllent/mailpit/issues/568))
- Add margin to icons in release and delete buttons for consistent spacing
- Update navbar theme to use data-bs-theme attribute for consistency
- Update Go dependencies
- Update node dependencies
## [v1.27.8]
### Chore
- Update Go dependencies
- Update node dependencies
- Update caniemail test database
## [v1.27.7]
### Fix
- Move HELO/EHLO hostname setting to the correct position in SMTP client creation ([#558](https://github.com/axllent/mailpit/issues/558))
## [v1.27.6]
### Feature
- Add optional --no-release-check to version subcommand ([#557](https://github.com/axllent/mailpit/issues/557))
### Chore
- Set HELO/EHLO hostname when connecting to external SMTP server ([#556](https://github.com/axllent/mailpit/issues/556))
- Update Go dependencies
- Update node dependencies
## [v1.27.5]
### Chore
- Update Go dependencies
- Update node dependencies
- Update caniemail test database
### Fix
- Support optional UIDL argument in POP3 server ([#552](https://github.com/axllent/mailpit/issues/552))
## [v1.27.4]
### Feature
- Allow rejected SMTP recipients to be silently dropped ([#549](https://github.com/axllent/mailpit/issues/549))
### Chore
- Update Go dependencies
- Update node dependencies
- Update caniemail test database
## [v1.27.3]
### Fix
- Fix sendmail when using an `--smtp-addr <ip>:<port>` ([#542](https://github.com/axllent/mailpit/issues/542))
## [v1.27.2]
### Security
- Prevent integer overflow conversion to uint64
- Add ReadHeaderTimeout to Prometheus metrics server
### Feature
- Add ability to generate self-signed (snakeoil) certificates for UI, SMTP and POP3 ([#539](https://github.com/axllent/mailpit/issues/539))
### Chore
- Allow sendmail to send to untrusted TLS server
- Update eslint config, remove neostandard
- Refactor JS functions and remove unused parameters
- Update Go dependencies
- Update node dependencies
### Fix
- Use MaxMessages to determine pruning ([#536](https://github.com/axllent/mailpit/issues/536))
- Support angle brackets for text/plain URLs with spaces ([#535](https://github.com/axllent/mailpit/issues/535))
- Do not check latest release for Prometheus statistics ([#522](https://github.com/axllent/mailpit/issues/522))
## [v1.27.1]
### Chore
- Allow unknown href link protocols in HTML view such as myapp:// ([#532](https://github.com/axllent/mailpit/issues/532))
- Update Go dependencies
- Update node dependencies
## [v1.27.0]
### Chore
- Remove unused functionality/deadcode (golangci-lint)
- Refactor error handling and resource management across multiple files (golangci-lint)
- Refactor API Swagger definitions and remove unused structs
- Bump minimum Go version to v1.24.3 for jhillyerd/enmime/v2
- Switch version checks & self-updater to use ghru/v2
- Update Go dependencies
- Update node dependencies
### Fix
- Align websocket new message values with global Message Summary (no null values) ([#526](https://github.com/axllent/mailpit/issues/526))
## [v1.26.2]
### Feature
- Store username with messages, auto-tag, and UI display ([#521](https://github.com/axllent/mailpit/issues/521))
- Allow version checking to be disabled ([#524](https://github.com/axllent/mailpit/issues/524))
### Chore
- Apply linting to all JavaScript/Vue files with eslint & prettier
- Update Go dependencies
- Update node dependencies
### Fix
- Improve version polling, add thread safety and exponential backoff ([#523](https://github.com/axllent/mailpit/issues/523))
### Test
- Add JavaScript linting tests to CI
- Add Go linting (gofmt) to CI
## [v1.26.1]
### Feature
- Add relay config to preserve (keep) original Message-IDs when relaying messages ([#515](https://github.com/axllent/mailpit/issues/515))
### Chore
- Update Go dependencies
- Update node dependencies
- Update caniemail testing database
### Fix
- Add optional message_num argument in POP3 LIST command ([#518](https://github.com/axllent/mailpit/issues/518))
- Use float64 for returned SQL value types for rqlite compatibility ([#520](https://github.com/axllent/mailpit/issues/520))
### Test
- Add small delay in POP3 test after disconnection to allow for background deletion in rqlite
- Add automated tests using the rqlite database
## [v1.26.0]
### Feature
- Send API allow separate auth ([#504](https://github.com/axllent/mailpit/issues/504))
- Add Prometheus exporter ([#505](https://github.com/axllent/mailpit/issues/505))
### Chore
- Add MP_DATA_FILE deprecation warning
- Update Go dependencies
- Update node dependencies
### Fix
- Ignore basic auth for OPTIONS requests to API when CORS is set
- Fix sendmail symlink detection for macOS ([#514](https://github.com/axllent/mailpit/issues/514))
## [v1.25.1]
### Chore
- Switch from unnecessary float64 to uint64 API values for App Information, message & attachment sizes
- Extend latest version cache expiration from 5 to 15 minutes
- Lighten outline-secondary buttons in dark mode
- Add note to swagger docs about API date formats
- Update Go dependencies
- Update node dependencies
### Fix
- Update bootstrap5-tags to fix text pasting in message release modal ([#498](https://github.com/axllent/mailpit/issues/498))
## [v1.25.0]
### Feature
- Add option to hide the "Delete all" button in web UI ([#495](https://github.com/axllent/mailpit/issues/495))
### Chore
- Upgrade to jhillyerd/enmime/v2
- Switch yaml parser to github.com/goccy/go-yaml
- Tweak UI to improve contrast between read & unread messages
- Adjust UI margin for side navigation
- Update Go dependencies
- Update node dependencies
- Update caniemail database
### Fix
- Include SMTPUTF8 capability in SMTP EHLO response ([#496](https://github.com/axllent/mailpit/issues/496))
### Documentation
- Switch to git-cliff for changelog generation
- Add Message ListUnsubscribe to swagger / API documentation ([#494](https://github.com/axllent/mailpit/issues/494))
## [v1.24.2]
### Feature
- Display unread count in app badge ([#485](https://github.com/axllent/mailpit/issues/485))
### Chore
- Install script improvements & better error handling ([#482](https://github.com/axllent/mailpit/issues/482))
- Update Go dependencies
- Update node dependencies
- Update caniemail database
## [v1.24.1]
### Feature
- Add ability to mark all search results as read ([#476](https://github.com/axllent/mailpit/issues/476))
### Chore
- Bump node version to 22 for binary releases
- Improve error message for From header parsing failure ([#477](https://github.com/axllent/mailpit/issues/477))
- Update Go dependencies
- Update node dependencies
## [v1.24.0]
### Feature
- Add TLS relay support and refactor relay function ([#471](https://github.com/axllent/mailpit/issues/471))
- Add TLS forwarding support and refactor forwarding function
### Chore
- Update Go dependencies
- Standardize error message casing
- Update Go dependencies
- Update node dependencies
## [v1.23.2]
### Chore
- Update node dependencies
- Use `Message-ID` header instead of `Message-Id` when generating new IDs (RFC 5322)
- Improve inline HTML Check style detection ([#467](https://github.com/axllent/mailpit/issues/467))
- Update Go dependencies
### Test
- Add tests for inline HTML Checks
## [v1.23.1]
### Chore
- Replace PrismJS with highlight.js for HTML syntax highlighting
- Update Go dependencies
- Update node dependencies
### Fix
- Allow searching messages using only Cyrillic characters ([#450](https://github.com/axllent/mailpit/issues/450))
- Prevent cropping bottom of label characters in web UI ([#457](https://github.com/axllent/mailpit/issues/457))
## [v1.23.0]
### Feature
- Add configuration to set message compression level in db (0-3) ([#447](https://github.com/axllent/mailpit/issues/447) & [#448](https://github.com/axllent/mailpit/issues/448))
- Add configuration to explicitly disable HTTP compression in web UI/API ([#448](https://github.com/axllent/mailpit/issues/448))
- Add configuration to disable SQLite WAL mode for NFS compatibility
### Chore
- Avoid shell in Docker health check ([#444](https://github.com/axllent/mailpit/issues/444))
- Handle BLOB storage for default database differently to rqlite to reduce memory overhead ([#447](https://github.com/axllent/mailpit/issues/447))
- Optimize ZSTD encoder for fastest compression of messages ([#447](https://github.com/axllent/mailpit/issues/447))
- Minor speed & memory improvements when storing messages
- Update Go dependencies
- Update node dependencies
### Fix
- Display the correct STARTTLS or TLS runtime option on startup ([#446](https://github.com/axllent/mailpit/issues/446))
### Test
- Add tests for message compression levels
## [v1.22.3]
### Feature
- Add dump feature to export all raw messages to a local directory ([#443](https://github.com/axllent/mailpit/issues/443))
### Chore
- Specify Docker health check start period and interval ([#439](https://github.com/axllent/mailpit/issues/439))
- Update Go dependencies
- Update node dependencies
### Fix
- Replace TrimLeft with TrimPrefix for webroot path handling ([#441](https://github.com/axllent/mailpit/issues/441))
- Include font/woff content type to embedded controller
- Update Swagger JSON to prevent overflow ([#442](https://github.com/axllent/mailpit/issues/442))
- Correctly detect maximum SMTP recipient limits, add test
## [v1.22.2]
### Chore
- Replace http.FileServer with custom controller to correctly encode gzipped error responses for embed.FS
- Enable browser cache for embedded web UI assets
- Update Go dependencies
- Update node dependencies / esbuild
### Fix
- Remove recursive HTML regeneration in embedded HTML view ([#434](https://github.com/axllent/mailpit/issues/434))
- Add missing "latest" route to message attachment API endpoint ([#437](https://github.com/axllent/mailpit/issues/437))
## [v1.22.1]
### Feature
- Add optional UI setting to skip "Delete all" & "Mark all read" confirmation dialogs([#428](https://github.com/axllent/mailpit/issues/428))
- Add optional query parameter for HTML message iframe embedding ([#434](https://github.com/axllent/mailpit/issues/434))
### Chore
- Bump actions/stale from 9.0.0 to 9.1.0 ([#432](https://github.com/axllent/mailpit/issues/432))
- Add API CORS policy to HTML preview routes ([#434](https://github.com/axllent/mailpit/issues/434))
- Update Go dependencies
- Update node dependencies
## [v1.22.0]
### Feature
- Add Chaos functionality to test integration handling of SMTP error responses ([#402](https://github.com/axllent/mailpit/issues/402), [#110](https://github.com/axllent/mailpit/issues/110), [#144](https://github.com/axllent/mailpit/issues/144) & [#268](https://github.com/axllent/mailpit/issues/268))
- Option to override the From email address in SMTP relay configuration ([#414](https://github.com/axllent/mailpit/issues/414))
- SMTP auto-forwarding option ([#414](https://github.com/axllent/mailpit/issues/414))
### Chore
- Update Go dependencies
- Update node dependencies
### Fix
- Correct date formatting in TestMakeHeaders
- Update command `npm run update-caniemail` save path ([#422](https://github.com/axllent/mailpit/issues/422))
## [v1.21.8]
### Chore
- Update Go dependencies
- Update node dependencies
### Fix
- Remove unused FOREIGN KEY REFERENCES in message_tags table ([#374](https://github.com/axllent/mailpit/issues/374))
## [v1.21.7]
### Chore
- Display "From" details in message sidebar (desktop) ([#403](https://github.com/axllent/mailpit/issues/403))
- Display "To" details in mobile messages list
- Stricter SMTP 'MAIL FROM' & 'RCPT TO' handling ([#409](https://github.com/axllent/mailpit/issues/409))
- Move smtpd & pop3 modules to internal
- Bump Go version for automated testing
- Update Go dependencies
- Update node dependencies
### Fix
- Prevent splitting multi-byte characters in message snippets ([#404](https://github.com/axllent/mailpit/issues/404))
- Ignore unsupported optional SMTP 'MAIL FROM' parameters ([#407](https://github.com/axllent/mailpit/issues/407))
### Test
- Add smtpd tests
## [v1.21.6]
### Feature
- Add support for sending inline attachments via HTTP API ([#399](https://github.com/axllent/mailpit/issues/399))
- Include Mailpit label (if set) in webhook HTTP header ([#400](https://github.com/axllent/mailpit/issues/400))
### Chore
- Update Go dependencies
- Update node dependencies
- Update caniemail database
### Fix
- Message view not updating when deleting messages from search ([#395](https://github.com/axllent/mailpit/issues/395))
## [v1.21.5]
### Chore
- Make symlink detection more specific to contain "sendmail" in the name ([#391](https://github.com/axllent/mailpit/issues/391))
- Update Go dependencies
- Update node dependencies
- Update caniemail database
## [v1.21.4]
### Bugfix
- Fix external CSS stylesheet loading in HTML preview ([#388](https://github.com/axllent/mailpit/issues/388))
## [v1.21.3]
### Chore
- Add swagger examples & API code restructure
- Upgrade Alpine packages on Docker build
- Update node dependencies
- Mute Dart Sass deprecation notices
- Minor UI tweaks
- Update Go dependencies
## [v1.21.2]
### Feature
- Add additional ignored flags to sendmail ([#384](https://github.com/axllent/mailpit/issues/384))
### Chore
- Update node dependencies
- Update Go dependencies
- Remove legacy Tags column from message DB table
### Fix
- Fix browser notification request on Edge ([#89](https://github.com/axllent/mailpit/issues/89))
## [v1.21.1]
### Feature
- Add ability to search for messages containing inline images (`has:inline`)
- Add ability to search by size smaller or larger than a value (eg: `larger:1M` / `smaller:2.5M`)
### Chore
- Separate attachments and inline images in download nav and badges ([#379](https://github.com/axllent/mailpit/issues/379))
- Update Go dependencies
## [v1.21.0]
### Feature
- Experimental Unix socket support for HTTPD & SMTPD ([#373](https://github.com/axllent/mailpit/issues/373))
### Fix
- Allow multiple item selection on macOS with Cmd-click ([#378](https://github.com/axllent/mailpit/issues/378))
## [v1.20.7]
### Chore
- Update caniemail database
### Fix
- SQL error deleting a tag while using tenant-id ([#374](https://github.com/axllent/mailpit/issues/374))
### Test
- Add tenantIDs to tests
## [v1.20.6]
### Chore
- Update node dependencies
- Update minimum Go version (1.22.0)
- Update Go dependencies
- Code cleanup
- Update swagger file tests
- Update node dependencies
- Bump Go compile version to 1.23
## [v1.20.5]
### Chore
- Improve link detection in the HTML preview
- Improve tag detection in UI
- Use consistent margins for Mailpit label if set
- Update node dependencies
### Fix
- Use correct parameter order in SpamAssassin socket detection ([#364](https://github.com/axllent/mailpit/issues/364))
## [v1.20.4]
### Chore
- Upgrade vue-css-donut-chart & related charts
- Update node dependencies
- Update Go dependencies
### Fix
- Relax URL detection in link check tool ([#357](https://github.com/axllent/mailpit/issues/357))
## [v1.20.3]
### Chore
- Do not recenter selected messages in sidebar on every new message
- Update Go dependencies
- Update node dependencies
- Update caniemail database
### Fix
- Disable automatic HTML/Text character detection when charset is provided ([#348](https://github.com/axllent/mailpit/issues/348))
## [v1.20.2]
### Feature
- Web UI notifications of smtpd & POP3 errors ([#347](https://github.com/axllent/mailpit/issues/347))
### Chore
- Add smtpd server logging in the CLI ([#347](https://github.com/axllent/mailpit/issues/347))
- Add debug database storage logging
- Update node dependencies
- Update Go dependencies
## [v1.20.1]
### Chore
- Show icon attachment in new side navigation message listing ([#345](https://github.com/axllent/mailpit/issues/345))
- Live load up to 100 new messages in sidebar ([#336](https://github.com/axllent/mailpit/issues/336))
- Shift inbox pagination to inbox component
### Fix
- Correctly decode X-Tags message headers (RFC 2047) ([#344](https://github.com/axllent/mailpit/issues/344))
## [v1.20.0]
### Feature
- List messages in side nav when viewing message for easy navigation ([#336](https://github.com/axllent/mailpit/issues/336))
- Add option to control message retention by age ([#338](https://github.com/axllent/mailpit/issues/338))
### Chore
- Make internal tagging methods private
- Update node dependencies
- Update Go dependencies
- Update caniemail database
### Fix
- Prevent Vue race condition to initialize dayjs relativeTime plugin
- Return `text/plain` header for message delete request
- Better regexp to detect tags in search
- Prevent potential JavaScript errors caused by race condition
## [v1.19.3]
### Security
- Prevent bypass of Contend Security Policy using stored XSS, and sanitize preview HTML data (DOMPurify)
### Chore
- Display nicer noscript message when JavaScript is disabled
- Update Go dependencies
## [v1.19.2]
### Chore
- Update Go dependencies
### Fix
- Update Inbox "Delete All" count when new messages are detected ([#334](https://github.com/axllent/mailpit/issues/334))
## [v1.19.1]
### Feature
- Add optional relay recipient blocklist ([#333](https://github.com/axllent/mailpit/issues/333))
### Chore
- Bump docker/build-push-action from 5 to 6 ([#327](https://github.com/axllent/mailpit/issues/327))
- Bump esbuild from 0.21.5 to 0.22.0 ([#326](https://github.com/axllent/mailpit/issues/326))
- Bump esbuild to version 0.23.0
- Equal column widths in About modal
- Update Go dependencies
## [v1.19.0]
### Feature
- Add option to disable auto-tagging for plus-addresses & X-Tags ([#323](https://github.com/axllent/mailpit/issues/323))
- Add ability to rename and delete tags globally
### Chore
- Update Go dependencies
- Update node dependencies
## [v1.18.7]
### Feature
- Add optional label to identify Mailpit instance ([#316](https://github.com/axllent/mailpit/issues/316))
### Chore
- Handle websocket errors caused by persistent connection failures ([#319](https://github.com/axllent/mailpit/issues/319))
- Refactor JavaScript, use arrow functions instead of "self" aliasing
### Test
- Add POP3 integration tests
## [v1.18.6]
### Chore
- Handle POP3 RSET command
- Delete multiple POP3 messages in single action
- Update Go dependencies
- Update node dependencies
- Update caniemail database
### Fix
- POP3 size output to show compatible sizes ([#312](https://github.com/axllent/mailpit/issues/312))
- POP3 end of file reached error ([#315](https://github.com/axllent/mailpit/issues/315))
## [v1.18.5]
### Feature
- Add pagination & limits to URL parameters ([#303](https://github.com/axllent/mailpit/issues/303))
### Chore
- Update Go dependencies
- Update node dependencies
## [v1.18.4]
### Chore
- Clone new Docker images to ghcr.io ([#302](https://github.com/axllent/mailpit/issues/302))
- Update Go dependencies
- Update node dependencies
## [v1.18.3]
### Feature
- ICalendar (ICS) viewer ([#298](https://github.com/axllent/mailpit/issues/298))
### Chore
- Update node dependencies
- Update Go dependencies
### Fix
- Add dot stuffing for POP3 ([#300](https://github.com/axllent/mailpit/issues/300))
## [v1.18.2]
### Chore
- Update node dependencies
### Fix
- Replace invalid Windows username characters in sendmail ([#294](https://github.com/axllent/mailpit/issues/294))
## [v1.18.1]
### Feature
- Return queued Message ID in SMTP response ([#293](https://github.com/axllent/mailpit/issues/293))
### Chore
- Simplify JSON HTTP responses
- Update Go dependencies
- Update node dependencies
## [v1.18.0]
### Feature
- New search filter prefix `addressed:` includes From, To, Cc, Bcc & Reply-To
- Search filter support for auto-tagging
- Set tagging filters via a config file
- API endpoint for sending ([#278](https://github.com/axllent/mailpit/issues/278))
### Chore
- Auto-update relative received message times
- Replace moment JS library with dayjs
- Improve tag sorting in web UI, ignore casing
- Remove function duplication - use common tools.InArray()
- JSON key case-consistency for posted API data (backwards-compatible)
- Update go-release-action
- Update Go dependencies
- Update node dependencies
## [v1.17.1]
### Chore
- Clearer error messages for read/write permission failures ([#281](https://github.com/axllent/mailpit/issues/281))
- Update Go dependencies
- Update node dependencies
### Fix
- Prevent error when two identical tags are added at the exact same time ([#283](https://github.com/axllent/mailpit/issues/283))
## [v1.17.0]
### Feature
- Add UI settings screen
- Option to auto relay for matching recipient expression only ([#274](https://github.com/axllent/mailpit/issues/274))
### Chore
- Remove deprecated --disable-html-check option
- Move Link check & HTML check features out of beta
- Update API documentation regarding date/time searches & timezones
- Replace disintegration/imaging with kovidgoyal/imaging to fix CVE-2023-36308
- Auto-rotate thumbnail images based on exif data
- Update Go dependencies
- Update node dependencies
- Update caniemail database
### Fix
- Add delay to close database on fatal exit ([#280](https://github.com/axllent/mailpit/issues/280))
## [v1.16.0]
### Feature
- Option to use rqlite database storage ([#254](https://github.com/axllent/mailpit/issues/254))
- Add optional tenant ID to isolate data in shared databases ([#254](https://github.com/axllent/mailpit/issues/254))
- Search support for before: and after: dates ([#252](https://github.com/axllent/mailpit/issues/252))
### Chore
- Switch database flag/env to `--database` / `MP_DATABASE`
- Update Go dependencies
- Update node dependencies
- Update caniemail test database
### Fix
- Extract plus addresses from email addresses only, not names
- Prevent conditional JS error when global mailbox tag list is modified via auto/plus-address tagging while viewing a message
- Remove duplicated authentication check ([#276](https://github.com/axllent/mailpit/issues/276))
## [v1.15.1]
### Feature
- Add readyz subcommand for Docker healthcheck ([#270](https://github.com/axllent/mailpit/issues/270))
### Chore
- Add labels to Docker image ([#267](https://github.com/axllent/mailpit/issues/267))
- Code cleanup, remove redundant functionality
## [v1.15.0]
### Feature
- Add SMTP TLS option ([#265](https://github.com/axllent/mailpit/issues/265))
### Chore
- Update Go dependencies
- Update node dependencies
### Fix
- Enforce SMTP STARTTLS by default if authentication is set
## [v1.14.4]
### Feature
- Allow setting SMTP relay configuration values via environment variables ([#262](https://github.com/axllent/mailpit/issues/262))
### Chore
- Reorder CLI flags to group by related functionality
- Update caniemail test data
## [v1.14.3]
### Chore
- Update Go dependencies
- Update node dependencies
### Fix
- Prevent crash when calculating deleted space percentage (divide by zero)
## [v1.14.2]
### Chore
- Allow setting of multiple message tags via plus addresses ([#253](https://github.com/axllent/mailpit/issues/253))
### Fix
- Prevent runtime error when calculating total messages size of empty table ([#263](https://github.com/axllent/mailpit/issues/263))
## [v1.14.1]
### Feature
- Set message tags using plus addressing ([#253](https://github.com/axllent/mailpit/issues/253))
- Option to enforce TitleCasing for all newly created tags
### Chore
- Update Go dependencies
- Update node dependencies
- Tag names now allow `.` and must be a minimum of 1 character
### Fix
- Handle null values in Mailpit settings, set DeletedSize=0 if null
## [v1.14.0]
### Feature
- Optional POP3 server ([#249](https://github.com/axllent/mailpit/issues/249))
### Chore
- Better handling of automatic database compression (vacuuming) after deleting messages
- Switch to short uuid format for database IDs
- Security improvements (gosec)
- Refactor storage library
- Update Go dependencies
- Update node dependencies
### Documentation
- Add edge Docker images for latest unreleased features
## [v1.13.3]
### Feature
- Add reply-to:<search> search filter ([#247](https://github.com/axllent/mailpit/issues/247))
### Chore
- Update "About" modal layout when new version is available
- Compress database only when >= 1% of total message size has been deleted
- Update Go dependencies
- Update node dependencies
### API
- Include Reply-To information in message summaries for message list & websocket events
## [v1.13.2]
### Feature
- Add option to log output to file ([#246](https://github.com/axllent/mailpit/issues/246))
### Chore
- Update esbuild
- Bump actions build requirement versions
- Update Go dependencies
- Update node dependencies
- Update caniemail data
## [v1.13.1]
### Feature
- Add TLSRequired option for smtpd ([#241](https://github.com/axllent/mailpit/issues/241))
### Chore
- Only show number of messages ignored statistics if `--ignore-duplicate-ids` is set
- Update Go dependencies
- Update node dependencies
### Fix
- Workaround for specific field searches containing unicode characters ([#239](https://github.com/axllent/mailpit/issues/239))
## [v1.13.0]
### Feature
- Add optional SpamAssassin integration to display scores ([#233](https://github.com/axllent/mailpit/issues/233))
- Display List-Unsubscribe & List-Unsubscribe-Post header info with syntax validation ([#236](https://github.com/axllent/mailpit/issues/236))
- Add option to disable SMTP reverse DNS (rDNS) lookup ([#230](https://github.com/axllent/mailpit/issues/230))
### Chore
- Update node dependencies
- Update Go dependencies
- Compress compiled assets with `npm run build`
### Fix
- Sendmail support for `-f 'Name <email@example.com>'` format
- Display multiple whitespace characters in message subject & recipient names ([#238](https://github.com/axllent/mailpit/issues/238))
## [v1.12.1]
### Feature
- Add option to only allow SMTP recipients matching a regular expression (disable open-relay behaviour [#219](https://github.com/axllent/mailpit/issues/219))
### Chore
- Standardize error logging & formatting
- Update node dependencies
- Automatically refresh connected browsers if Mailpit is upgraded (version change)
- Significantly increase database performance using WAL (Write-Ahead-Log)
### Fix
- Log total deleted messages when deleting all messages from search
- Prevent rare error from websocket connection (unexpected non-whitespace character)
- Log total deleted messages when auto-pruning messages (--max)
### Test
- Run tests on Linux, Windows & Mac
## [v1.12.0]
### Chore
- Refresh search results when search resubmitted or active tag filter clicked
- Standardize error logging & formatting
- Convert to many-to-many message tag relationships
- Update Go dependencies
- Update node dependencies
- Update caniemail test data
- Use memory pointer for internal message parsing & storage
- Include runtime statistics in API (info) & UI (About)
## [v1.11.1]
### Chore
- Allow multiple tags to be searched using Ctrl-click ([#216](https://github.com/axllent/mailpit/issues/216))
- Update Go dependencies
- Update node dependencies
### Fix
- Fix regression to support for search query params to all `/latest` endpoints ([#206](https://github.com/axllent/mailpit/issues/206))
### Test
- Add new `ingest` subcommand to import an email file or maildir folder over SMTP
## [v1.11.0]
### Feature
- Add configuration option to set maximum SMTP recipients ([#205](https://github.com/axllent/mailpit/issues/205))
### Chore
- Update Go dependencies
- Update node dependencies
### API
- Allow ID "latest" for message summary, headers, raw version & HTML/link checks
## [v1.10.4]
### Fix
- Remove JS debug information for favicon
## [v1.10.3]
### Feature
- Add @ as valid character for webroot ([#215](https://github.com/axllent/mailpit/issues/215))
### Chore
- Update caniemail library & add `hr` element test
- Update Go dependencies
- Update node dependencies
### Fix
- New favicon notification badge to fix rendering issues ([#210](https://github.com/axllent/mailpit/issues/210))
## [v1.10.2]
### Feature
- Allow port binding using hostname
### Chore
- Clearer log messages for bound SMTP & HTTP addresses
- Add favicon fallback font (sans-serif) for unread count
- Update Go dependencies
- Update node dependencies
- Enable tag colors by default
## [v1.10.1]
### Chore
- Use NextReader() instead of ReadMessage() for websocket reading ([#207](https://github.com/axllent/mailpit/issues/207))
- Update Go dependencies
- Update node dependencies
### Fix
- Prevent JavaScript error if message is missing `From` header ([#209](https://github.com/axllent/mailpit/issues/209))
### Documentation
- Revert BinaryResponse type to string
## [v1.10.0]
### Feature
- Add URL redirect (`/view/latest`) to view latest message in web UI ([#166](https://github.com/axllent/mailpit/issues/166))
- Option to allow untrusted HTTPS certificates for screenshots & link checking ([#204](https://github.com/axllent/mailpit/issues/204))
- Support search query params to /latest endpoints ([#206](https://github.com/axllent/mailpit/issues/206))
### Chore
- Update Go dependencies
- Update node dependencies
### Fix
- Correctly close websockets on client disconnect ([#207](https://github.com/axllent/mailpit/issues/207))
## [v1.9.10]
### Chore
- Fix column width in search view
- Update caniemail test data
- Update Go dependencies
- Update node dependencies
### Fix
- Correctly display "About" modal when update check fails (resolves [#199](https://github.com/axllent/mailpit/issues/199))
### Documentation
- Update documentation links
## [v1.9.9]
### Feature
- Reset message date on release ([#194](https://github.com/axllent/mailpit/issues/194))
- Set optional webhook for received messages ([#195](https://github.com/axllent/mailpit/issues/195))
### Chore
- Move html2text module to internal/html2text
- Update Go dependencies
- Update node dependencies
## [v1.9.8]
### Chore
- Replace html2text modules with simplified internal function
- Replace satori/go.uuid with github.com/google/uuid ([#190](https://github.com/axllent/mailpit/issues/190))
- Update Go dependencies
- Update node dependencies
### Documentation
- Update swagger documentation
### Test
- Add html2text tests
- Add test to validate swagger.json
## [v1.9.7]
### Chore
- Update Go dependencies & minimum Go version (1.21)
- Downgrade microcosm-cc/bluemonday, revert to Go 1.20
- Update node dependencies
### Fix
- Enable delete button when new messages arrive
## [v1.9.6]
### Chore
- Display message previews on separate line ([#175](https://github.com/axllent/mailpit/issues/175))
- Update Go dependencies
- Update node dependencies
## [v1.9.5]
### Feature
- Display email previews ([#175](https://github.com/axllent/mailpit/issues/175))
- Add `reindex` subcommand to reindex all messages
### Fix
- Correctly detect tags in search (UI)
- HTML message preview background color when switching themes in Chrome
### Test
- Add snippet tests
- Add message summary tests
## [v1.9.4]
### Feature
- Set auth credentials directly from environment variables
### Chore
- Add option to delete a message after release
- Remove some flags deprecated 08/2022
- Update Go dependencies
- Update node dependencies
## [v1.9.3]
### Chore
- Only queue broadcast events if clients are connected
- Move utils/* packages to internal/*
- Update internal import paths
- Move storage package to internal/storage
- Update internal/storage import paths
- Display "Loading messages" instead of "No results" while loading results
- Do not show excluded search tags as "current" in nav
### Test
- Add tests for ArgsParser & CleanTag
- Add more API tests
- Add endpoints for integration tests
## [v1.9.2]
### Chore
- Reset pagination when returning to inbox from search
- Update node dependencies
### Fix
- Delete all messages matching search when more than 1000 results
### Test
- Add search delete tests
- Add message tag tests
## [v1.9.1]
### Chore
- Better support for mobile screen sizes
- Link email addresses in message summary to search
- Update Go dependencies
- Update caniemail data
- Set 404 page when loading a non-existent message
## [v1.9.0]
### Feature
- New search filter `[!]is:tagged`
- Improved search parser
### Chore
- Update node dependencies
- Rewrite web UI, add URL routing and components
- Update Go dependencies
- Update minimum Go version to 1.20
### API
- Add endpoint to return all tags in use
- Delete by search filter
- Remove redundant `Read` status from message (always true)
### Fix
- Correctly escape certain characters in search (eg: `'`)
### Test
- Bump Go version to 1.21
## [v1.8.4]
### Fix
- Correctly decode proxy links containing HTML entities (screenshots)
## [v1.8.3]
### Feature
- HTML screenshots
### Chore
- Group message tabs on mobile
- Update node dependencies
## [v1.8.2]
### Feature
- Workaround for non-RFC-compliant message headers containing <CR><CR><LF>
- Link check to test message links
### Chore
- Set hostname in page meta title to identify Mailpit instance
- Update Go libs
### Build
- Update wangyoucao577/go-release-action@v1.39
## [v1.8.1]
### Chore
- Update Go dependencies
- Update node dependencies
### Fix
- Exclude <script type="application/json"> from HTML check tests
- Exclude "sendmail" from recipients list when using `mailpit sendmail <options>`
- Check/set message Reply-To using SMTP FROM
### Documentation
- Add pagination to swagger search documentation
## [v1.8.0]
### Feature
- HTML check to test & score mail client compatibility with HTML emails
### Chore
- Pagination support for search, all results
- Remove `<base />` tag if set in HTML preview
- Add flag to block all access to remote CSS and fonts (CSP)
- Update Go dependencies
- Update node dependencies
### Fix
- Add basePath to swagger.json if webroot is specified
### Documentation
- Update swagger docs
- Update brew installation instructions
## [v1.7.1]
### Chore
- Update node dependencies
- Update dark mode loading background color
- Dark mode color adjustments
- Wrap HTML source lines
- Update Go dependencies
## [v1.7.0]
### Chore
- Theme toggler - auto, light and dark themes
- Update Go dependencies
- Update node dependencies
### API
- Set raw message Content-Type to UTF-8
- Ignore SMTP relay error when one of multiple recipients doesn't exist
### Build
- Define Vue build options in esbuild
## [v1.6.22]
### Feature
- Clearer SMTP error messages
### Chore
- Upgrade node dependencies
- Update Go dependencies
## [v1.6.21]
### Chore
- More accurate clickable hyperlink logic in plain text messages
## [v1.6.20]
### Feature
- Convert links into clickable hyperlinks in plain text message content
### Chore
- Update node dependencies
## [v1.6.19]
### Fix
- Only display sendmail help when sendmail subcommand is invoked
## [v1.6.18]
### Chore
- Display message tags below subject in message overview
- Add option to enable tag colors based on tag name hash
### API
- Sort tags before saving
## [v1.6.17]
### Fix
- Add single dash arguments support to sendmail command ([#123](https://github.com/axllent/mailpit/issues/123))
## [v1.6.16]
### Bugfix
- Fix sendmail/startup panic
## [v1.6.15]
### Feature
- Add `sendmail -bs` functionality
## [v1.6.14]
### Feature
- Set tags via X-Tags message header
- Add ability to delete or mark search results read
### Chore
- Update node dependencies
## [v1.6.13]
### Feature
- Add SMTP LOGIN authentication method for message relay
## [v1.6.12]
### Feature
- Add Message-Id to MessageSummary ([#116](https://github.com/axllent/mailpit/issues/116))
### Documentation
- Update swagger field descriptions, add MessageID
## [v1.6.11]
### Chore
- Check for secure context instead of HTTPS ([#114](https://github.com/axllent/mailpit/issues/114))
- Update Go dependencies
- Update node dependencies
## [v1.6.10]
### Chore
- Remove "Noto Color Emoji" from default bootstrap font list
- Update Go dependencies
- Update node dependencies
## [v1.6.9]
### Chore
- Update Go dependencies
- Update node dependencies
### API
- Return blank 200 response for OPTIONS requests (CORS)
### Bugfix
- Correctly escape JS cid regex
## [v1.6.8]
### Feature
- Add `-S` short flag for sendmail `--smtp-addr`
- Add allowlist to filter recipients before relaying messages ([#109](https://github.com/axllent/mailpit/issues/109))
### Bugfix
- Fix Date display when message doesn't contain a Date header
## [v1.6.7]
### Bugfix
- Fix auto-deletion cron
## [v1.6.6]
### Feature
- Option to ignore duplicate Message-IDs
### Chore
- Style Undisclosed recipients in message view
- Update Go dependencies
- Update node dependencies
### API
- Include correct start value in search response
- Set Access-Control-Allow-Headers when --api-cors is set
### Documentation
- Update swagger field descriptions
## [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]
### Feature
- Inject/update Bcc header for missing addresses when SMTP recipients do not match message headers
### Chore
- Update node dependencies
- Update Go dependencies
- Message release functionality
- Display Return-Path if different to the From address
### API
- Include Return-Path in message summary data
- Message relay / release
- Enable cross-origin resource sharing (CORS) configuration
## [v1.5.5]
### Feature
- Update listen regex to allow IPv6 addresses ([#85](https://github.com/axllent/mailpit/issues/85))
### Documentation
- Add Docker image tag for major/minor version
## [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]
### Chore
- Tab to view formatted message headers
### API
- Include Reply-To in message summary (including Web UI)
## [v1.5.1]
### Feature
- Add 'o', 'b' & 's' ignored flags for sendmail
### Chore
- Update node dependencies
- Update Go dependencies
## [v1.5.0]
### Feature
- Option to use message dates as received dates (new messages only)
- Options to support auth without STARTTLS, and accept any login
- Rename SSL to TLS, add deprecation warnings to flags & ENV variables referring to SSL
- Download raw message, HTML/text body parts or attachments via single button
- OpenAPI / Swagger schema
### API
- Return received datetime when message does not contain a date header
### Bugfix
- Fix JavaScript error when adding the first tag manually
## [v1.4.0]
### Feature
- Option to use message dates as received dates (new messages only)
- Options to support auth without STARTTLS, and accept any login
- Rename SSL to TLS, add deprecation warnings to flags & ENV variables referring to SSL
### API
- Return received datetime when message does not contain a date header
## [v1.3.11]
### Feature
- Expand custom webroot path to include a-z A-Z 0-9 _ . - and /
### Documentation
- Expose default ports (1025/tcp 8025/tcp)
## [v1.3.10]
### Chore
- Update node dependencies
### Bugfix
- Fix search with existing emails
## [v1.3.9]
### Feature
- Add Cc and Bcc search filters
### Chore
- Update Go dependencies
- Update node dependencies
## [v1.3.8]
### Chore
- Compress SVG icons
### Bugfix
- Restore notification icon
## [v1.3.7]
### Feature
- Add Kubernetes API health (livez/readyz) endpoints
### Chore
- Upgrade to esbuild 0.17.5
## [v1.3.6]
### Chore
- Update Go dependencies
- Update node dependencies
### Bugfix
- Correctly index missing 'From' header in database
## [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]
### Chore
- Rename "results" to "result" when singular message returned
- Upgrade esbuild & axios
### Bugfix
- Append trailing slash to custom webroot for UI & API
## [v1.3.0]
### Chore
- Update node dependencies
- Update Go dependencies
### Build
- Remove duplicate bootstrap CSS
## [v1.2.9]
### Bugfix
- Delay 200ms to set `target="_blank"` for all rendered email links
## [v1.2.8]
### Feature
- Message tags and auto-tagging
### Bugfix
- Return empty arrays rather than null for message To, CC, BCC, Inlines & Attachments
## [v1.2.7]
### Feature
- Allow custom webroot
## [v1.2.6]
### Chore
- Update node dependencies
- Update Go dependencies
### API
- Provide structs of API v1 responses for use in client code
## [1.2.5]
### Chore
- Bump build action to use node 18
- Theme changes
- Load first page if paginated list returns 0 results
- Broadcast "delete all" action to reload all connected clients
## [1.2.4]
### Bugfix
- Fix mail download link
## [1.2.3]
### Chore
- Prevent double message index request on websocket connect
### API
- Add limit and start parameters to search
## [1.2.2]
### Chore
- Update Go dependencies
### API
- Add API endpoint to return message headers
### Test
- Add API test for raw & message headers
## [1.2.1]
### Chore
- Add about app modal with version update notification
- Update frontend modules
## [1.2.0]
### Feature
- Add REST API
### Chore
- Hide delete all / mark all read in message view
- Changes to use new data API
### Test
- Add API tests
## [1.1.7]
### Chore
- Add documentation link (wiki)
### Fix
- Workaround for Safari source matching bug blocking event listener
- Normalize running binary name detection (Windows)
## [1.1.5]
### Chore
- Support for inline images using filenames instead of cid
### Build
- Switch to esbuild-sass-plugin
## [1.1.4]
### Security
- Add restrictive HTTP Content-Security-Policy
### Feature
- Add --quiet flag to display only errors
### Chore
- Remove left & right borders (message list)
- Add favicon unread message counter
- Minor UI color change & unread count position adjustment
## [1.1.3]
### Fix
- Update message download link
## [1.1.2]
### Chore
- Allow reverse proxy subdirectories
## [1.1.1]
### Chore
- Attachment icons and image thumbnails
## [1.1.0]
### Chore
- Add previous/next message links
- HTML source & highlighting
## [1.0.0]
### Feature
- Search parser improvements
- Search parser improvements
- Multiple message selection for group actions using shift/ctrl click
### Chore
- Update frontend modules & esbuild
- Update frontend modules & esbuild
- Display unknown recipients as as `Undisclosed recipients`
- Post data using 'application/json'
## [1.0.0-beta1]
### Feature
- Switch backend storage to use SQLite
### Chore
- Resize preview iframe on load
## [0.1.5]
### Feature
- Improved message search - any order & phrase quoting
### Chore
- Resize iframes with viewport resize
- Change breakpoints for mobile view of messages
## [0.1.4]
### Feature
- Email compression in storage
### Chore
- Mobile compatibility improvements & functionality
### Test
- Database total/unread statistics tests
- Enable testing on feature branches
## [0.1.3]
### Feature
- Mark all messages as read
### Chore
- Update pagination values when new mail arrives when not on first page
- Minor UI tweaks
- Add reset search button
- Better error handling when connection to server is broken
## [0.1.2]
### Security
- Use strconv.Atoi() for safe string to int conversions
- Sanitize mailbox names
- Don't allow tar files containing a ".."
### Feature
- Optional browser notifications (HTTPS only)
## [0.1.1]
### Bugfix
- Fix env variable for MP_UI_SSL_KEY
## [0.1.0]
### Feature
- SMTP STARTTLS & SMTP authentication support
## [0.0.9]
### Feature
- HTTPS option for web UI
### Test
- Memory & physical database tests
### Bugfix
- Include read status in search results
## [0.0.8]
### Chore
- Add project links to help in CLI
### Bugfix
- Fix total/unread count after failed message inserts
## [0.0.7]
### Feature
- : Add multi-arch docker image
### Bugfix
- Command flag should be `--auth-file`
## [0.0.6]
### Bugfix
- Disable CGO when building multi-arch binaries
## [0.0.5]
### Feature
- Basic authentication support
## [0.0.4]
### Chore
- Cater for messages without From email address
- Add space in To fields
- Minor UI & logging changes
- Cater for messages without From email address
- Add space in To fields
- Add date to console log
### Test
- Add search tests
### Bugfix
- Update to clover-v2.0.0-alpha.2 to fix sorting
## [0.0.3]
### Bugfix
- Update to clover-v2.0.0-alpha.2 to fix sorting
## [0.0.2]
### Feature
- Unread statistics
## [0.0.1-beta]
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Mailpit
Thank you for your interest in contributing to Mailpit!
## Reporting issues and feature requests
If you find a bug or have a feature request, please [open an issue](https://github.com/axllent/mailpit/issues) and provide as much detail as possible. Please **do not** report security issues here (see below).
## Reporting security issues
Please do not report security issues publicly in GitHub. Refer to [SECURITY document](https://github.com/axllent/mailpit/blob/develop/.github/SECURITY.md) for instructions and contact information.
## Contributing code
Please ensure your code is clean and well-commented, and [passes linting](https://mailpit.axllent.org/docs/development/code-linting/) before submitting a Pull Request. Contributions should enhance the functionality or usability of Mailpit, focusing on quality over quantity.
Note that while assistance from AI tools is perfectly acceptable, **"[vibe coded](https://en.wikipedia.org/wiki/Vibe_coding)" pull requests will most likely not be accepted.**
We value the unique insights and creativity that individual contributors bring to the project.
Thank you for your understanding and for contributing to Mailpit!
================================================
FILE: Dockerfile
================================================
FROM golang:alpine AS builder
ARG VERSION=dev
COPY . /app
WORKDIR /app
RUN apk upgrade && apk add 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
LABEL org.opencontainers.image.title="Mailpit" \
org.opencontainers.image.description="An email and SMTP testing tool with API for developers" \
org.opencontainers.image.source="https://github.com/axllent/mailpit" \
org.opencontainers.image.url="https://mailpit.axllent.org" \
org.opencontainers.image.documentation="https://mailpit.axllent.org/docs/" \
org.opencontainers.image.licenses="MIT"
COPY --from=builder /mailpit /mailpit
RUN apk upgrade --no-cache && apk add --no-cache tzdata
EXPOSE 1025/tcp 1110/tcp 8025/tcp
HEALTHCHECK --interval=15s --start-period=10s --start-interval=1s CMD ["/mailpit", "readyz"]
ENTRYPOINT ["/mailpit"]
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2022-Now() Ralph Slooten
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
<h1 align="center">
Mailpit - email testing for developers
</h1>
<div align="center">
<a href="https://github.com/axllent/mailpit/actions/workflows/tests.yml"><img src="https://github.com/axllent/mailpit/actions/workflows/tests.yml/badge.svg" alt="CI Tests status"></a>
<a href="https://github.com/axllent/mailpit/actions/workflows/release-build.yml"><img src="https://github.com/axllent/mailpit/actions/workflows/release-build.yml/badge.svg" alt="CI build status"></a>
<a href="https://github.com/axllent/mailpit/actions/workflows/build-docker.yml"><img src="https://github.com/axllent/mailpit/actions/workflows/build-docker.yml/badge.svg" alt="CI Docker build status"></a>
<a href="https://github.com/axllent/mailpit/actions/workflows/codeql-analysis.yml"><img src="https://github.com/axllent/mailpit/actions/workflows/codeql-analysis.yml/badge.svg" alt="Code quality"></a>
<a href="https://goreportcard.com/report/github.com/axllent/mailpit"><img src="https://goreportcard.com/badge/github.com/axllent/mailpit" alt="Go Report Card"></a>
<br>
<a href="https://github.com/axllent/mailpit/releases/latest"><img src="https://img.shields.io/github/v/release/axllent/mailpit.svg" alt="Latest release"></a>
<a href="https://hub.docker.com/r/axllent/mailpit"><img src="https://img.shields.io/docker/pulls/axllent/mailpit.svg" alt="Docker pulls"></a>
</div>
<br>
<p align="center">
<a href="https://mailpit.axllent.org">Website</a> •
<a href="https://mailpit.axllent.org/docs/">Documentation</a> •
<a href="https://mailpit.axllent.org/docs/api-v1/">API</a>
</p>
<hr>
**Mailpit** is a small, fast, low memory, zero-dependency, multi-platform email testing tool & API for developers.
It acts as an SMTP server, provides a modern web interface to view & test captured emails, and includes an API for automated integration testing.
Mailpit was originally **inspired** by MailHog which is [no longer maintained](https://github.com/mailhog/MailHog/issues/442#issuecomment-1493415258) and hasn't seen active development or security updates for a few years now.

## Features
- Runs entirely from a single [static binary](https://mailpit.axllent.org/docs/install/) or multi-architecture [Docker images](https://mailpit.axllent.org/docs/install/docker/)
- Modern web UI with advanced [mail search](https://mailpit.axllent.org/docs/usage/search-filters/) to view emails (formatted HTML, highlighted HTML source, text, headers, raw source, and MIME attachments
including image thumbnails), including optional [HTTPS](https://mailpit.axllent.org/docs/configuration/http/) & [authentication](https://mailpit.axllent.org/docs/configuration/http/)
- [SMTP server](https://mailpit.axllent.org/docs/configuration/smtp/) with optional STARTTLS or SSL/TLS, authentication (including an "accept any" mode)
- A [REST API](https://mailpit.axllent.org/docs/api-v1/) for integration testing
- Real-time web UI updates using web sockets for new mail & optional [browser notifications](https://mailpit.axllent.org/docs/usage/notifications/) when new mail is received
- Optional [POP3 server](https://mailpit.axllent.org/docs/configuration/pop3/) to download captured message directly into your email client
- [HTML check](https://mailpit.axllent.org/docs/usage/html-check/) to test & score mail client compatibility with HTML emails
- [Link check](https://mailpit.axllent.org/docs/usage/link-check/) to test message links (HTML & text) & linked images
- [Spam check](https://mailpit.axllent.org/docs/usage/spamassassin/) to test message "spamminess" using a running SpamAssassin server
- [Create screenshots](https://mailpit.axllent.org/docs/usage/html-screenshots/) of HTML messages via web UI
- Mobile and tablet HTML preview toggle in desktop mode
- [Message tagging](https://mailpit.axllent.org/docs/usage/tagging/) including manual tagging or automated tagging using filtering and "plus addressing"
- [SMTP relaying](https://mailpit.axllent.org/docs/configuration/smtp-relay/) (message release) - relay messages via a different SMTP server including an optional allowlist of accepted recipients
- [SMTP forwarding](https://mailpit.axllent.org/docs/configuration/smtp-forward/) - automatically forward messages via a different SMTP server to predefined email addresses
- Fast message [storing & processing](https://mailpit.axllent.org/docs/configuration/email-storage/) - ingesting 100-200 emails per second over SMTP depending on CPU, network speed & email size,
easily handling tens of thousands of emails, with automatic email pruning (by default keeping the most recent 500 emails)
- [Chaos](https://mailpit.axllent.org/docs/integration/chaos/) feature to enable configurable SMTP errors to test application resilience
- `List-Unsubscribe` syntax validation
- Optional [webhook](https://mailpit.axllent.org/docs/integration/webhook/) for received messages
## 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 package managers
- **Mac**: `brew install mailpit` (to run automatically in the background: `brew services start mailpit`)
- **Arch Linux**: available in the AUR as `mailpit`
- **FreeBSD**: `pkg install mailpit`
### Install via script (Linux & Mac)
Linux & Mac users can install it directly to `/usr/local/bin/mailpit` with:
```shell
sudo sh < <(curl -sL https://raw.githubusercontent.com/axllent/mailpit/develop/install.sh)
```
You can also change the install path to something else by setting the `INSTALL_PATH` environment, for example:
```shell
sudo INSTALL_PATH=/usr/bin sh < <(curl -sL https://raw.githubusercontent.com/axllent/mailpit/develop/install.sh)
```
### 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 be extracted and copied to your `$PATH`, or simply run as `./mailpit`.
### Docker
See [Docker instructions](https://mailpit.axllent.org/docs/install/docker/) for 386, amd64 & arm64 images.
### Compile from source
To build Mailpit from source, see [Building from source](https://mailpit.axllent.org/docs/install/source/).
## Usage
Run `mailpit -h` to see options. More information can be seen in [the docs](https://mailpit.axllent.org/docs/configuration/runtime-options/).
If installed using homebrew, you may run `brew services start mailpit` to always run mailpit automatically.
### Testing Mailpit
Please refer to [the documentation](https://mailpit.axllent.org/docs/install/testing/) on how to easily test email delivery to Mailpit.
### Configuring sendmail
Mailpit's SMTP server (default on port 1025), so you will likely need to configure your sending application to deliver mail via that port.
A common MTA (Mail Transfer Agent) that delivers system emails to an SMTP server is `sendmail`, used by many applications, including PHP.
Mailpit can also act as substitute for sendmail. For instructions on how to set this up, please refer to the [sendmail documentation](https://mailpit.axllent.org/docs/install/sendmail/).
---
<p align="center">
For team features, multiple inboxes, and a hosted setup, try
<a href="https://mailtrap.io/?ref=mailpit">Mailtrap</a>, our friendly companion.
</p>
================================================
FILE: cmd/dump.go
================================================
package cmd
import (
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/dump"
"github.com/axllent/mailpit/internal/logger"
"github.com/spf13/cobra"
)
// dumpCmd represents the dump command
var dumpCmd = &cobra.Command{
Use: "dump <database> <output-dir>",
Short: "Dump all messages from a database to a directory",
Long: `Dump all messages stored in Mailpit into a local directory as individual files.
The database can either be the database file (eg: --database /var/lib/mailpit/mailpit.db) or a
URL of a running Mailpit instance (eg: --http http://127.0.0.1/). If dumping over HTTP, the URL
should be the base URL of your running Mailpit instance, not the link to the API itself.`,
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
if err := dump.Sync(args[0]); err != nil {
logger.Log().Fatal(err)
}
},
}
func init() {
rootCmd.AddCommand(dumpCmd)
dumpCmd.Flags().SortFlags = false
dumpCmd.Flags().StringVar(&config.Database, "database", config.Database, "Dump messages directly from a database file")
dumpCmd.Flags().StringVar(&config.TenantID, "tenant-id", config.TenantID, "Database tenant ID to isolate data (optional)")
dumpCmd.Flags().StringVar(&dump.URL, "http", dump.URL, "Dump messages via HTTP API (base URL of running Mailpit instance)")
dumpCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
}
================================================
FILE: cmd/ingest.go
================================================
package cmd
import (
"bytes"
"fmt"
"io"
"net/mail"
"os"
"path/filepath"
"strings"
"time"
"github.com/axllent/mailpit/internal/logger"
sendmail "github.com/axllent/mailpit/sendmail/cmd"
"github.com/spf13/cobra"
)
var (
ingestRecent int
)
// ingestCmd represents the ingest command
var ingestCmd = &cobra.Command{
Use: "ingest <file|folder> ...[file|folder]",
Short: "Ingest a file or folder of emails for testing",
Long: `Ingest a file or folder of emails for testing.
This command will scan the folder for emails and deliver them via SMTP to a running
Mailpit server. Each email must be a separate file (eg: Maildir format, not mbox).
The --recent flag will only consider files with a modification date within the last X days.`,
// Hidden: true,
Args: cobra.MinimumNArgs(1),
Run: func(_ *cobra.Command, args []string) {
var count int
var total int
var per100start = time.Now()
for _, a := range args {
err := filepath.Walk(a,
func(path string, info os.FileInfo, err error) error {
if err != nil {
logger.Log().Error(err)
return nil
}
if !isFile(path) {
return nil
}
if ingestRecent > 0 && time.Since(info.ModTime()) > time.Duration(ingestRecent)*24*time.Hour {
return nil
}
f, err := os.Open(filepath.Clean(path))
if err != nil {
logger.Log().Errorf("%s: %s", path, err.Error())
return nil
}
defer func() { _ = f.Close() }()
body, err := io.ReadAll(f)
if err != nil {
logger.Log().Errorf("%s: %s", path, err.Error())
return nil
}
msg, err := mail.ReadMessage(bytes.NewReader(body))
if err != nil {
logger.Log().Errorf("error parsing message body: %s", err.Error())
return nil
}
recipients := []string{}
// get all recipients in To, Cc and Bcc
if to, err := msg.Header.AddressList("To"); err == nil {
for _, a := range to {
recipients = append(recipients, a.Address)
}
}
if cc, err := msg.Header.AddressList("Cc"); err == nil {
for _, a := range cc {
recipients = append(recipients, a.Address)
}
}
if bcc, err := msg.Header.AddressList("Bcc"); err == nil {
for _, a := range bcc {
recipients = append(recipients, a.Address)
}
}
if sendmail.FromAddr == "" {
if fromAddresses, err := msg.Header.AddressList("From"); err == nil {
sendmail.FromAddr = fromAddresses[0].Address
}
}
if len(recipients) == 0 {
// Bcc
recipients = []string{sendmail.FromAddr}
}
returnPath := strings.Trim(msg.Header.Get("Return-Path"), "<>")
if returnPath == "" {
if fromAddresses, err := msg.Header.AddressList("From"); err == nil {
returnPath = fromAddresses[0].Address
}
}
err = sendmail.Send(sendmail.SMTPAddr, returnPath, recipients, body)
if err != nil {
logger.Log().Errorf("error sending mail: %s (%s)", err.Error(), path)
return nil
}
count++
total++
if count%100 == 0 {
logger.Log().Infof("[%s] 100 messages in %s", format(total), time.Since(per100start))
per100start = time.Now()
}
return nil
})
if err != nil {
logger.Log().Error(err)
}
}
},
}
func init() {
rootCmd.AddCommand(ingestCmd)
ingestCmd.Flags().StringVarP(&sendmail.SMTPAddr, "smtp-addr", "S", sendmail.SMTPAddr, "SMTP server address")
ingestCmd.Flags().IntVarP(&ingestRecent, "recent", "r", 0, "Only ingest messages from the last X days (default all)")
}
// IsFile returns if a path is a file
func isFile(path string) bool {
info, err := os.Stat(path)
if os.IsNotExist(err) || !info.Mode().IsRegular() {
return false
}
return true
}
// Format a an integer 10000 => 10,000
func format(n int) string {
in := fmt.Sprintf("%d", n)
numOfDigits := len(in)
if n < 0 {
numOfDigits-- // First character is the - sign (not a digit)
}
numOfCommas := (numOfDigits - 1) / 3
out := make([]byte, len(in)+numOfCommas)
if n < 0 {
in, out[0] = in[1:], '-'
}
for i, j, k := len(in)-1, len(out)-1, 0; ; i, j = i-1, j-1 {
out[j] = in[i]
if i == 0 {
return string(out)
}
if k++; k == 3 {
j, k = j-1, 0
out[j] = ','
}
}
}
================================================
FILE: cmd/readyz.go
================================================
package cmd
import (
"crypto/tls"
"fmt"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/axllent/mailpit/config"
"github.com/spf13/cobra"
)
var (
useHTTPS bool
)
// readyzCmd represents the healthcheck command
var readyzCmd = &cobra.Command{
Use: "readyz",
Short: "Run a healthcheck to test if Mailpit is running",
Long: `This command connects to the /readyz endpoint of a running Mailpit server
and exits with a status of 0 if the connection is successful, else with a
status 1 if unhealthy.
If running within Docker, it should automatically detect environment
settings to determine the HTTP bind interface & port.
`,
Run: func(_ *cobra.Command, _ []string) {
webroot := strings.TrimRight(path.Join("/", config.Webroot, "/"), "/") + "/"
proto := "http"
if useHTTPS {
proto = "https"
}
uri := fmt.Sprintf("%s://%s%sreadyz", proto, config.HTTPListen, webroot)
conf := &http.Transport{
IdleConnTimeout: time.Second * 5,
ExpectContinueTimeout: time.Second * 5,
TLSHandshakeTimeout: time.Second * 5,
// do not verify TLS if this instance is using HTTPS as we connect using IP
// so won't be the same as the cert
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec
}
client := &http.Client{Transport: conf}
res, err := client.Get(uri)
if err != nil || res.StatusCode != 200 {
os.Exit(1)
}
},
}
func init() {
rootCmd.AddCommand(readyzCmd)
if len(os.Getenv("MP_UI_BIND_ADDR")) > 0 {
config.HTTPListen = os.Getenv("MP_UI_BIND_ADDR")
}
if len(os.Getenv("MP_WEBROOT")) > 0 {
config.Webroot = os.Getenv("MP_WEBROOT")
}
config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
if config.UITLSCert != "" {
useHTTPS = true
}
readyzCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "Set the HTTP bind interface & port")
readyzCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
readyzCmd.Flags().BoolVar(&useHTTPS, "https", useHTTPS, "Connect via HTTPS (ignores HTTPS validation)")
}
================================================
FILE: cmd/reindex.go
================================================
package cmd
import (
"os"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/spf13/cobra"
)
// reindexCmd represents the reindex command
var reindexCmd = &cobra.Command{
Use: "reindex <database>",
Short: "Reindex the database",
Long: `This will reindex all messages in the entire database.
If you have several thousand messages in your mailbox, then it is advised to shut down
Mailpit while you reindex as this process will likely result in database locking issues.`,
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
config.Database = args[0]
config.MaxMessages = 0
if err := storage.InitDB(); err != nil {
logger.Log().Error(err)
os.Exit(1)
}
storage.ReindexAll()
},
}
func init() {
rootCmd.AddCommand(reindexCmd)
}
================================================
FILE: cmd/root.go
================================================
// Package cmd is the main application
package cmd
import (
"os"
"strconv"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/prometheus"
"github.com/axllent/mailpit/internal/smtpd"
"github.com/axllent/mailpit/internal/smtpd/chaos"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server"
"github.com/axllent/mailpit/server/webhook"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "mailpit",
Short: "Mailpit is an email testing tool for developers",
Long: `Mailpit is an email testing tool for developers.
It acts as an SMTP server, and provides a web interface to view all captured emails.
Documentation:
https://github.com/axllent/mailpit
https://mailpit.axllent.org/docs/`,
Run: func(_ *cobra.Command, _ []string) {
if err := config.VerifyConfig(); err != nil {
logger.Log().Error(err.Error())
os.Exit(1)
}
if err := storage.InitDB(); err != nil {
logger.Log().Fatal(err.Error())
os.Exit(1)
}
// Start Prometheus metrics if enabled
switch prometheus.GetMode() {
case "integrated":
prometheus.StartUpdater()
case "separate":
go prometheus.StartSeparateServer()
}
go server.Listen()
if err := smtpd.Listen(); err != nil {
storage.Close()
logger.Log().Fatal(err.Error())
os.Exit(1)
}
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// hide autocompletion
rootCmd.CompletionOptions.HiddenDefaultCmd = true
rootCmd.Flags().SortFlags = false
// hide help command
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
// hide help flag
rootCmd.PersistentFlags().BoolP("help", "h", false, "This help")
rootCmd.PersistentFlags().Lookup("help").Hidden = true
// load and warn deprecated ENV vars
initDeprecatedConfigFromEnv()
// load environment variables
initConfigFromEnv()
rootCmd.Flags().StringVarP(&config.Database, "database", "d", config.Database, "Database to store persistent data")
rootCmd.Flags().BoolVar(&config.DisableWAL, "disable-wal", config.DisableWAL, "Disable WAL for local database (allows NFS mounted DBs)")
rootCmd.Flags().BoolVar(&config.DisableVersionCheck, "disable-version-check", config.DisableVersionCheck, "Disable version update checking")
rootCmd.Flags().IntVar(&config.Compression, "compression", config.Compression, "Compression level to store raw messages (0-3)")
rootCmd.Flags().StringVar(&config.Label, "label", config.Label, "Optional label identify this Mailpit instance")
rootCmd.Flags().StringVar(&config.TenantID, "tenant-id", config.TenantID, "Database tenant ID to isolate data")
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
rootCmd.Flags().StringVar(&config.MaxAge, "max-age", config.MaxAge, "Max age of messages in either (h)ours or (d)ays (eg: 3d)")
rootCmd.Flags().BoolVar(&config.UseMessageDates, "use-message-dates", config.UseMessageDates, "Use message dates as the received dates")
rootCmd.Flags().BoolVar(&config.IgnoreDuplicateIDs, "ignore-duplicate-ids", config.IgnoreDuplicateIDs, "Ignore duplicate messages (by Message-ID)")
rootCmd.Flags().StringVar(&logger.LogFile, "log-file", logger.LogFile, "Log output to file instead of stdout")
rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
// Web UI / API
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface & port for UI")
rootCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI & API authentication")
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key")
rootCmd.Flags().StringVar(&config.UITLSKey, "ui-tls-key", config.UITLSKey, "TLS key for web UI (HTTPS) - requires ui-tls-cert")
rootCmd.Flags().StringVar(&server.AccessControlAllowOrigin, "api-cors", server.AccessControlAllowOrigin, "Set CORS origin(s) for the API, comma-separated (eg: example.com,foo.com)")
rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts")
rootCmd.Flags().BoolVar(&config.AllowInternalHTTPRequests, "allow-internal-http-requests", config.AllowInternalHTTPRequests, "Allow link-checker & screenshots to access internal IP addresses")
rootCmd.Flags().StringVar(&config.EnableSpamAssassin, "enable-spamassassin", config.EnableSpamAssassin, "Enable integration with SpamAssassin")
rootCmd.Flags().BoolVar(&config.AllowUntrustedTLS, "allow-untrusted-tls", config.AllowUntrustedTLS, "Do not verify HTTPS certificates (link checker & screenshots)")
rootCmd.Flags().BoolVar(&config.DisableHTTPCompression, "disable-http-compression", config.DisableHTTPCompression, "Disable HTTP compression support (web UI & API)")
rootCmd.Flags().BoolVar(&config.HideDeleteAllButton, "hide-delete-all-button", config.HideDeleteAllButton, "Hide the \"Delete all\" button in the web UI")
// Send API
rootCmd.Flags().StringVar(&config.SendAPIAuthFile, "send-api-auth-file", config.SendAPIAuthFile, "A password file for Send API authentication")
rootCmd.Flags().BoolVar(&config.SendAPIAuthAcceptAny, "send-api-auth-accept-any", config.SendAPIAuthAcceptAny, "Accept any username and password for the Send API endpoint, including none")
// SMTP server
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
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.SMTPRequireSTARTTLS, "smtp-require-starttls", config.SMTPRequireSTARTTLS, "Require SMTP client use STARTTLS")
rootCmd.Flags().BoolVar(&config.SMTPRequireTLS, "smtp-require-tls", config.SMTPRequireTLS, "Require client use SSL/TLS")
rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", config.SMTPAuthAllowInsecure, "Allow insecure PLAIN & LOGIN SMTP authentication")
rootCmd.Flags().BoolVar(&config.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain <CR><CR><LF>")
rootCmd.Flags().IntVar(&config.SMTPMaxRecipients, "smtp-max-recipients", config.SMTPMaxRecipients, "Maximum SMTP recipients allowed")
rootCmd.Flags().StringVar(&config.SMTPAllowedRecipients, "smtp-allowed-recipients", config.SMTPAllowedRecipients, "Only allow SMTP recipients matching a regular expression (default allow all)")
rootCmd.Flags().BoolVar(&config.SMTPIgnoreRejectedRecipients, "smtp-ignore-rejected-recipients", config.SMTPIgnoreRejectedRecipients, "Ignore rejected SMTP recipients with 2xx response")
rootCmd.Flags().BoolVar(&smtpd.DisableReverseDNS, "smtp-disable-rdns", smtpd.DisableReverseDNS, "Disable SMTP reverse DNS lookups")
// SMTP relay
rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP relay configuration file to allow releasing messages")
rootCmd.Flags().BoolVar(&config.SMTPRelayAll, "smtp-relay-all", config.SMTPRelayAll, "Auto-relay all new messages via external SMTP server (caution!)")
rootCmd.Flags().StringVar(&config.SMTPRelayMatching, "smtp-relay-matching", config.SMTPRelayMatching, "Auto-relay new messages to only matching recipients (regular expression)")
// SMTP forwarding
rootCmd.Flags().StringVar(&config.SMTPForwardConfigFile, "smtp-forward-config", config.SMTPForwardConfigFile, "SMTP forwarding configuration file for all messages")
// Chaos
rootCmd.Flags().BoolVar(&chaos.Enabled, "enable-chaos", chaos.Enabled, "Enable Chaos functionality (API / web UI)")
rootCmd.Flags().StringVar(&config.ChaosTriggers, "chaos-triggers", config.ChaosTriggers, "Enable Chaos & set the triggers for SMTP server")
// POP3 server
rootCmd.Flags().StringVar(&config.POP3Listen, "pop3", config.POP3Listen, "POP3 server bind interface and port")
rootCmd.Flags().StringVar(&config.POP3AuthFile, "pop3-auth-file", config.POP3AuthFile, "A password file for POP3 server authentication (enables POP3 server)")
rootCmd.Flags().StringVar(&config.POP3TLSCert, "pop3-tls-cert", config.POP3TLSCert, "Optional TLS certificate for POP3 server - requires pop3-tls-key")
rootCmd.Flags().StringVar(&config.POP3TLSKey, "pop3-tls-key", config.POP3TLSKey, "Optional TLS key for POP3 server - requires pop3-tls-cert")
// Tagging
rootCmd.Flags().StringVarP(&config.CLITagsArg, "tag", "t", config.CLITagsArg, "Tag new messages matching filters")
rootCmd.Flags().StringVar(&config.TagsConfig, "tags-config", config.TagsConfig, "Load tags filters from yaml configuration file")
rootCmd.Flags().BoolVar(&tools.TagsTitleCase, "tags-title-case", tools.TagsTitleCase, "TitleCase new tags generated from plus-addresses and X-Tags")
rootCmd.Flags().StringVar(&config.TagsDisable, "tags-disable", config.TagsDisable, "Disable auto-tagging, comma separated (eg: plus-addresses,x-tags)")
rootCmd.Flags().BoolVar(&config.TagsUsername, "tags-username", config.TagsUsername, "Auto-tag messages with the authenticated username")
// Prometheus metrics
rootCmd.Flags().StringVar(&config.PrometheusListen, "enable-prometheus", config.PrometheusListen, "Enable Prometheus metrics: true|false|<ip:port> (eg:'0.0.0.0:9090')")
// Webhook
rootCmd.Flags().StringVar(&config.WebhookURL, "webhook-url", config.WebhookURL, "Send a webhook request for new messages")
rootCmd.Flags().IntVar(&webhook.RateLimit, "webhook-limit", webhook.RateLimit, "Limit webhook requests per second")
rootCmd.Flags().IntVar(&webhook.Delay, "webhook-delay", webhook.Delay, "Delay in seconds before sending webhook requests (default 0)")
// DEPRECATED FLAG 2024/04/12 - but will not be removed to maintain backwards compatibility
rootCmd.Flags().StringVar(&config.Database, "db-file", config.Database, "Database file to store persistent data")
rootCmd.Flags().Lookup("db-file").Hidden = true
// 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"
// DEPRECATED FLAGS 2024/03/16
rootCmd.Flags().BoolVar(&config.SMTPRequireSTARTTLS, "smtp-tls-required", config.SMTPRequireSTARTTLS, "smtp-require-starttls")
rootCmd.Flags().Lookup("smtp-tls-required").Hidden = true
rootCmd.Flags().Lookup("smtp-tls-required").Deprecated = "use --smtp-require-starttls"
// DEPRECATED FLAG 2024/04/13 - no longer used
rootCmd.Flags().BoolVar(&config.DisableHTMLCheck, "disable-html-check", config.DisableHTMLCheck, "Disable the HTML check functionality (web UI & API)")
rootCmd.Flags().Lookup("disable-html-check").Hidden = true
}
// Load settings from environment
func initConfigFromEnv() {
// General
if len(os.Getenv("MP_DATABASE")) > 0 {
config.Database = os.Getenv("MP_DATABASE")
}
config.DisableWAL = getEnabledFromEnv("MP_DISABLE_WAL")
config.DisableVersionCheck = getEnabledFromEnv("MP_DISABLE_VERSION_CHECK")
if len(os.Getenv("MP_COMPRESSION")) > 0 {
config.Compression, _ = strconv.Atoi(os.Getenv("MP_COMPRESSION"))
}
config.TenantID = os.Getenv("MP_TENANT_ID")
config.Label = os.Getenv("MP_LABEL")
if len(os.Getenv("MP_MAX_MESSAGES")) > 0 {
config.MaxMessages, _ = strconv.Atoi(os.Getenv("MP_MAX_MESSAGES"))
}
if len(os.Getenv("MP_MAX_AGE")) > 0 {
config.MaxAge = os.Getenv("MP_MAX_AGE")
}
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
config.UseMessageDates = true
}
if getEnabledFromEnv("MP_IGNORE_DUPLICATE_IDS") {
config.IgnoreDuplicateIDs = true
}
if len(os.Getenv("MP_LOG_FILE")) > 0 {
logger.LogFile = os.Getenv("MP_LOG_FILE")
}
if getEnabledFromEnv("MP_QUIET") {
logger.QuietLogging = true
}
if getEnabledFromEnv("MP_VERBOSE") {
logger.VerboseLogging = true
}
// Web UI & API
if len(os.Getenv("MP_UI_BIND_ADDR")) > 0 {
config.HTTPListen = os.Getenv("MP_UI_BIND_ADDR")
}
if len(os.Getenv("MP_WEBROOT")) > 0 {
config.Webroot = os.Getenv("MP_WEBROOT")
}
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
if err := auth.SetUIAuth(os.Getenv("MP_UI_AUTH")); err != nil {
logger.Log().Error(err.Error())
}
config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
config.UITLSKey = os.Getenv("MP_UI_TLS_KEY")
if len(os.Getenv("MP_API_CORS")) > 0 {
server.AccessControlAllowOrigin = os.Getenv("MP_API_CORS")
}
if getEnabledFromEnv("MP_BLOCK_REMOTE_CSS_AND_FONTS") {
config.BlockRemoteCSSAndFonts = true
}
if getEnabledFromEnv("MP_ALLOW_INTERNAL_HTTP_REQUESTS") {
config.AllowInternalHTTPRequests = true
}
if len(os.Getenv("MP_ENABLE_SPAMASSASSIN")) > 0 {
config.EnableSpamAssassin = os.Getenv("MP_ENABLE_SPAMASSASSIN")
}
if getEnabledFromEnv("MP_ALLOW_UNTRUSTED_TLS") {
config.AllowUntrustedTLS = true
}
if getEnabledFromEnv("MP_DISABLE_HTTP_COMPRESSION") {
config.DisableHTTPCompression = true
}
if getEnabledFromEnv("MP_HIDE_DELETE_ALL_BUTTON") {
config.HideDeleteAllButton = true
}
// Send API
config.SendAPIAuthFile = os.Getenv("MP_SEND_API_AUTH_FILE")
if err := auth.SetSendAPIAuth(os.Getenv("MP_SEND_API_AUTH")); err != nil {
logger.Log().Error(err.Error())
}
if getEnabledFromEnv("MP_SEND_API_AUTH_ACCEPT_ANY") {
config.SendAPIAuthAcceptAny = true
}
// SMTP server
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")
}
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
if err := auth.SetSMTPAuth(os.Getenv("MP_SMTP_AUTH")); err != nil {
logger.Log().Error(err.Error())
}
if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") {
config.SMTPAuthAcceptAny = true
}
config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
if getEnabledFromEnv("MP_SMTP_REQUIRE_STARTTLS") {
config.SMTPRequireSTARTTLS = true
}
if getEnabledFromEnv("MP_SMTP_REQUIRE_TLS") {
config.SMTPRequireTLS = true
}
if getEnabledFromEnv("MP_SMTP_AUTH_ALLOW_INSECURE") {
config.SMTPAuthAllowInsecure = true
}
if getEnabledFromEnv("MP_SMTP_STRICT_RFC_HEADERS") {
config.SMTPStrictRFCHeaders = true
}
if len(os.Getenv("MP_SMTP_MAX_RECIPIENTS")) > 0 {
config.SMTPMaxRecipients, _ = strconv.Atoi(os.Getenv("MP_SMTP_MAX_RECIPIENTS"))
}
if len(os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")) > 0 {
config.SMTPAllowedRecipients = os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")
}
if getEnabledFromEnv("MP_SMTP_IGNORE_REJECTED_RECIPIENTS") {
config.SMTPIgnoreRejectedRecipients = true
}
if getEnabledFromEnv("MP_SMTP_DISABLE_RDNS") {
smtpd.DisableReverseDNS = true
}
// SMTP relay
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
if getEnabledFromEnv("MP_SMTP_RELAY_ALL") {
config.SMTPRelayAll = true
}
config.SMTPRelayMatching = os.Getenv("MP_SMTP_RELAY_MATCHING")
config.SMTPRelayConfig = config.SMTPRelayConfigStruct{}
config.SMTPRelayConfig.Host = os.Getenv("MP_SMTP_RELAY_HOST")
if len(os.Getenv("MP_SMTP_RELAY_PORT")) > 0 {
config.SMTPRelayConfig.Port, _ = strconv.Atoi(os.Getenv("MP_SMTP_RELAY_PORT"))
}
config.SMTPRelayConfig.STARTTLS = getEnabledFromEnv("MP_SMTP_RELAY_STARTTLS")
config.SMTPRelayConfig.TLS = getEnabledFromEnv("MP_SMTP_RELAY_TLS")
config.SMTPRelayConfig.AllowInsecure = getEnabledFromEnv("MP_SMTP_RELAY_ALLOW_INSECURE")
config.SMTPRelayConfig.Auth = os.Getenv("MP_SMTP_RELAY_AUTH")
config.SMTPRelayConfig.Username = os.Getenv("MP_SMTP_RELAY_USERNAME")
config.SMTPRelayConfig.Password = os.Getenv("MP_SMTP_RELAY_PASSWORD")
config.SMTPRelayConfig.Secret = os.Getenv("MP_SMTP_RELAY_SECRET")
config.SMTPRelayConfig.ReturnPath = os.Getenv("MP_SMTP_RELAY_RETURN_PATH")
config.SMTPRelayConfig.OverrideFrom = os.Getenv("MP_SMTP_RELAY_OVERRIDE_FROM")
config.SMTPRelayConfig.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS")
config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS")
config.SMTPRelayConfig.PreserveMessageIDs = getEnabledFromEnv("MP_SMTP_RELAY_PRESERVE_MESSAGE_IDS")
config.SMTPRelayConfig.ForwardSMTPErrors = getEnabledFromEnv("MP_SMTP_RELAY_FWD_SMTP_ERRORS")
// SMTP forwarding
config.SMTPForwardConfigFile = os.Getenv("MP_SMTP_FORWARD_CONFIG")
config.SMTPForwardConfig = config.SMTPForwardConfigStruct{}
config.SMTPForwardConfig.Host = os.Getenv("MP_SMTP_FORWARD_HOST")
if len(os.Getenv("MP_SMTP_FORWARD_PORT")) > 0 {
config.SMTPForwardConfig.Port, _ = strconv.Atoi(os.Getenv("MP_SMTP_FORWARD_PORT"))
}
config.SMTPForwardConfig.STARTTLS = getEnabledFromEnv("MP_SMTP_FORWARD_STARTTLS")
config.SMTPForwardConfig.TLS = getEnabledFromEnv("MP_SMTP_FORWARD_TLS")
config.SMTPForwardConfig.AllowInsecure = getEnabledFromEnv("MP_SMTP_FORWARD_ALLOW_INSECURE")
config.SMTPForwardConfig.Auth = os.Getenv("MP_SMTP_FORWARD_AUTH")
config.SMTPForwardConfig.Username = os.Getenv("MP_SMTP_FORWARD_USERNAME")
config.SMTPForwardConfig.Password = os.Getenv("MP_SMTP_FORWARD_PASSWORD")
config.SMTPForwardConfig.Secret = os.Getenv("MP_SMTP_FORWARD_SECRET")
config.SMTPForwardConfig.ReturnPath = os.Getenv("MP_SMTP_FORWARD_RETURN_PATH")
config.SMTPForwardConfig.OverrideFrom = os.Getenv("MP_SMTP_FORWARD_OVERRIDE_FROM")
config.SMTPForwardConfig.To = os.Getenv("MP_SMTP_FORWARD_TO")
config.SMTPForwardConfig.ForwardSMTPErrors = getEnabledFromEnv("MP_SMTP_FORWARD_FWD_SMTP_ERRORS")
// Chaos
chaos.Enabled = getEnabledFromEnv("MP_ENABLE_CHAOS")
config.ChaosTriggers = os.Getenv("MP_CHAOS_TRIGGERS")
// POP3 server
if len(os.Getenv("MP_POP3_BIND_ADDR")) > 0 {
config.POP3Listen = os.Getenv("MP_POP3_BIND_ADDR")
}
config.POP3AuthFile = os.Getenv("MP_POP3_AUTH_FILE")
if err := auth.SetPOP3Auth(os.Getenv("MP_POP3_AUTH")); err != nil {
logger.Log().Error(err.Error())
}
config.POP3TLSCert = os.Getenv("MP_POP3_TLS_CERT")
config.POP3TLSKey = os.Getenv("MP_POP3_TLS_KEY")
// Tagging
config.CLITagsArg = os.Getenv("MP_TAG")
config.TagsConfig = os.Getenv("MP_TAGS_CONFIG")
tools.TagsTitleCase = getEnabledFromEnv("MP_TAGS_TITLE_CASE")
config.TagsDisable = os.Getenv("MP_TAGS_DISABLE")
config.TagsUsername = getEnabledFromEnv("MP_TAGS_USERNAME")
// Prometheus metrics
if len(os.Getenv("MP_ENABLE_PROMETHEUS")) > 0 {
config.PrometheusListen = os.Getenv("MP_ENABLE_PROMETHEUS")
}
// Webhook
if len(os.Getenv("MP_WEBHOOK_URL")) > 0 {
config.WebhookURL = os.Getenv("MP_WEBHOOK_URL")
}
if len(os.Getenv("MP_WEBHOOK_LIMIT")) > 0 {
webhook.RateLimit, _ = strconv.Atoi(os.Getenv("MP_WEBHOOK_LIMIT"))
}
if len(os.Getenv("MP_WEBHOOK_DELAY")) > 0 {
webhook.Delay, _ = strconv.Atoi(os.Getenv("MP_WEBHOOK_DELAY"))
}
// Demo mode
config.DemoMode = getEnabledFromEnv("MP_DEMO_MODE")
}
// load deprecated settings from environment and warn
func initDeprecatedConfigFromEnv() {
// deprecated 2024/04/12 - but will not be removed to maintain backwards compatibility
if len(os.Getenv("MP_DATA_FILE")) > 0 {
logger.Log().Warn("ENV MP_DATA_FILE has been deprecated, use MP_DATABASE")
config.Database = os.Getenv("MP_DATA_FILE")
}
// deprecated 2023/03/12
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
logger.Log().Warn("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")
config.UITLSCert = os.Getenv("MP_UI_SSL_CERT")
}
// deprecated 2023/03/12
if len(os.Getenv("MP_UI_SSL_KEY")) > 0 {
logger.Log().Warn("ENV MP_UI_SSL_KEY has been deprecated, use MP_UI_TLS_KEY")
config.UITLSKey = os.Getenv("MP_UI_SSL_KEY")
}
// deprecated 2023/03/12
if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 {
logger.Log().Warn("ENV MP_SMTP_CERT has been deprecated, use MP_SMTP_TLS_CERT")
config.SMTPTLSCert = os.Getenv("MP_SMTP_SSL_CERT")
}
// deprecated 2023/03/12
if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 {
logger.Log().Warn("ENV MP_SMTP_KEY has been deprecated, use MP_SMTP_TLS_KEY")
config.SMTPTLSKey = os.Getenv("MP_SMTP_SMTP_KEY")
}
// deprecated 2023/12/10
if getEnabledFromEnv("MP_STRICT_RFC_HEADERS") {
logger.Log().Warn("ENV MP_STRICT_RFC_HEADERS has been deprecated, use MP_SMTP_STRICT_RFC_HEADERS")
config.SMTPStrictRFCHeaders = true
}
// deprecated 2024/03.16
if getEnabledFromEnv("MP_SMTP_TLS_REQUIRED") {
logger.Log().Warn("ENV MP_SMTP_TLS_REQUIRED has been deprecated, use MP_SMTP_REQUIRE_STARTTLS")
config.SMTPRequireSTARTTLS = true
}
if getEnabledFromEnv("MP_DISABLE_HTML_CHECK") {
logger.Log().Warn("ENV MP_DISABLE_HTML_CHECK has been deprecated and is no longer used")
config.DisableHTMLCheck = true
}
}
// 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
}
================================================
FILE: cmd/sendmail.go
================================================
package cmd
import (
"os"
sendmail "github.com/axllent/mailpit/sendmail/cmd"
"github.com/spf13/cobra"
)
// sendmailCmd represents the sendmail command
var sendmailCmd = &cobra.Command{
Use: "sendmail [flags] [recipients]",
Short: "A sendmail command replacement for Mailpit",
Run: func(_ *cobra.Command, _ []string) {
sendmail.Run()
},
}
func init() {
rootCmd.AddCommand(sendmailCmd)
var ignored string
// print out manual help screen
sendmailCmd.SetHelpTemplate(sendmail.HelpTemplate([]string{os.Args[0], "sendmail"}))
// these are simply repeated for cli consistency as cobra/viper does not allow
// multi-letter single-dash variables (-bs)
sendmailCmd.Flags().StringVarP(&sendmail.FromAddr, "from", "f", sendmail.FromAddr, "SMTP sender")
sendmailCmd.Flags().StringVarP(&sendmail.SMTPAddr, "smtp-addr", "S", sendmail.SMTPAddr, "SMTP server address")
sendmailCmd.Flags().BoolVarP(&sendmail.UseB, "ignored-b", "b", false, "Handle SMTP commands on standard input (use as -bs)")
sendmailCmd.Flags().BoolVarP(&sendmail.UseS, "ignored-s", "s", false, "Handle SMTP commands on standard input (use as -bs)")
sendmailCmd.Flags().BoolP("verbose", "v", false, "Verbose mode (sends debug output to stderr)")
sendmailCmd.Flags().BoolP("ignored-i", "i", false, "Ignored")
sendmailCmd.Flags().BoolP("ignored-o", "o", false, "Ignored")
sendmailCmd.Flags().BoolP("ignored-t", "t", false, "Ignored")
sendmailCmd.Flags().StringVarP(&ignored, "ignored-name", "F", "", "Ignored")
sendmailCmd.Flags().StringVarP(&ignored, "ignored-bits", "B", "", "Ignored")
sendmailCmd.Flags().StringVarP(&ignored, "ignored-errors", "e", "", "Ignored")
}
================================================
FILE: cmd/version.go
================================================
package cmd
import (
"fmt"
"os"
"runtime"
"github.com/axllent/mailpit/config"
"github.com/spf13/cobra"
)
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "Display the current version & update information",
Long: `Display the current version & update information (if available).`,
Run: func(cmd *cobra.Command, _ []string) {
update, _ := cmd.Flags().GetBool("update")
noReleaseCheck, _ := cmd.Flags().GetBool("no-release-check")
if update {
// Update the application
rel, err := config.GHRUConfig.SelfUpdate()
if err != nil {
fmt.Printf("Error updating: %s\n", err)
os.Exit(1)
}
fmt.Printf("Updated %s to version %s\n", os.Args[0], rel.Tag)
os.Exit(0)
}
fmt.Printf("%s %s compiled with %s on %s/%s\n",
os.Args[0], config.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
if !noReleaseCheck {
release, err := config.GHRUConfig.Latest()
if err != nil {
fmt.Printf("Error checking for latest release: %s\n", err)
os.Exit(1)
}
// The latest version is the same version
if release.Tag == config.Version {
os.Exit(0)
}
// A newer release is available
fmt.Printf(
"\nUpdate available: %s\nRun `%s version -u` to update (requires read/write access to install directory).\n",
release.Tag,
os.Args[0],
)
}
},
}
func init() {
rootCmd.AddCommand(versionCmd)
versionCmd.Flags().
BoolP("update", "u", false, "update to latest version")
versionCmd.Flags().
Bool("no-release-check", false, "do not check online for the latest release version")
}
================================================
FILE: config/config.go
================================================
// Package config handles the application configuration
package config
import (
"errors"
"fmt"
"net"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/axllent/ghru/v2"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/smtpd/chaos"
"github.com/axllent/mailpit/internal/snakeoil"
"github.com/axllent/mailpit/internal/spamassassin"
"github.com/axllent/mailpit/internal/tools"
)
var (
// Version is the Mailpit version, updated with every release
Version = "dev"
// GHRUConfig is the configuration for the GitHub Release Updater
// used to check for updates and self-update
GHRUConfig = ghru.Config{
Repo: "axllent/mailpit",
ArchiveName: "mailpit-{{.OS}}-{{.Arch}}",
BinaryName: "mailpit",
CurrentVersion: Version,
}
// SMTPListen to listen on <interface>:<port>
SMTPListen = "[::]:1025"
// HTTPListen to listen on <interface>:<port>
HTTPListen = "[::]:8025"
// Database for mail (optional)
Database string
// DisableWAL will disable Write-Ahead Logging in SQLite
// @see https://sqlite.org/wal.html
DisableWAL bool
// Compression is the compression level used to store raw messages in the database:
// 0 = off, 1 = fastest (default), 2 = standard, 3 = best compression
Compression = 1
// TenantID is an optional prefix to be applied to all database tables,
// allowing multiple isolated instances of Mailpit to share a database.
TenantID string
// Label to identify this Mailpit instance (optional).
// This gets applied to web UI, SMTP and optional POP3 server.
Label string
// MaxMessages is the maximum number of messages a mailbox can have (auto-pruned every minute)
MaxMessages = 500
// MaxAge is the maximum age of messages (auto-pruned every hour).
// Value can be either <int>h for hours or <int>d for days
MaxAge string
// MaxAgeInHours is the maximum age of messages in hours, set with parseMaxAge() using MaxAge value
MaxAgeInHours int
// UseMessageDates sets the Created date using the message date, not the delivered date
UseMessageDates bool
// UITLSCert file
UITLSCert string
// UITLSKey file
UITLSKey string
// UIAuthFile for UI & API authentication
UIAuthFile string
// Webroot to define the base path for the UI and API
Webroot = "/"
// DisableHTTPCompression will explicitly disable HTTP compression in the web UI and API
DisableHTTPCompression bool
// SendAPIAuthFile for Send API authentication
SendAPIAuthFile string
// SendAPIAuthAcceptAny accepts any username/password for the send API endpoint, including none
SendAPIAuthAcceptAny bool
// SMTPTLSCert file
SMTPTLSCert string
// SMTPTLSKey file
SMTPTLSKey string
// SMTPRequireSTARTTLS to enforce the use of STARTTLS
// The only allowed commands are NOOP, EHLO, STARTTLS and QUIT (as specified in RFC 3207) until
// the connection is upgraded to TLS i.e. until STARTTLS is issued.
SMTPRequireSTARTTLS bool
// SMTPRequireTLS to allow only SSL/TLS connections for all connections
//
SMTPRequireTLS bool
// SMTPAuthFile for SMTP authentication
SMTPAuthFile string
// SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication
SMTPAuthAllowInsecure bool
// SMTPAuthAcceptAny accepts any username/password including none
SMTPAuthAcceptAny bool
// SMTPMaxRecipients is the maximum number of recipients a message may have.
// The SMTP RFC states that an server must handle a minimum of 100 recipients
// however some servers accept more.
SMTPMaxRecipients = 100
// IgnoreDuplicateIDs will skip messages with the same ID
IgnoreDuplicateIDs bool
// BlockRemoteCSSAndFonts used to disable remote CSS & fonts
BlockRemoteCSSAndFonts = false
// AllowInternalHTTPRequests will allow HTTP requests to internal IP addresses (e.g., loopback, private, link-local, or multicast) when set to true.
// This policy applies to both link checking and screenshot generation (proxy) features and is disabled by default for security reasons.
AllowInternalHTTPRequests = false
// CLITagsArg is used to map the CLI args
CLITagsArg string
// ValidTagRegexp represents a valid tag
ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_\.@]){1,100}$`)
// TagsConfig is a yaml file to pre-load tags
TagsConfig string
// TagFilters are used to apply tags to new mail
TagFilters []autoTag
// TagsDisable accepts a comma-separated list of tag types to disable
// including x-tags & plus-addresses
TagsDisable string
// TagsUsername enables auto-tagging messages with the authenticated username
TagsUsername bool
// SMTPRelayConfigFile to parse a yaml file and store config of the relay SMTP server
SMTPRelayConfigFile string
// SMTPRelayConfig to parse a yaml file and store config of the the relay SMTP server
SMTPRelayConfig SMTPRelayConfigStruct
// ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile
ReleaseEnabled = false
// SMTPRelayAll is whether to relay all incoming messages via pre-configured SMTP server.
// Use with extreme caution!
SMTPRelayAll = false
// SMTPRelayMatching if set, will auto-release to recipients matching this regular expression
SMTPRelayMatching string
// SMTPRelayMatchingRegexp is the compiled version of SMTPRelayMatching
SMTPRelayMatchingRegexp *regexp.Regexp
// SMTPForwardConfigFile to parse a yaml file and store config of the forwarding SMTP server
SMTPForwardConfigFile string
// SMTPForwardConfig to parse a yaml file and store config of the forwarding SMTP server
SMTPForwardConfig SMTPForwardConfigStruct
// SMTPStrictRFCHeaders will return an error if the email headers contain <CR><CR><LF> (\r\r\n)
// @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153
SMTPStrictRFCHeaders bool
// SMTPAllowedRecipients if set, will only accept recipients matching this regular expression
SMTPAllowedRecipients string
// SMTPAllowedRecipientsRegexp is the compiled version of SMTPAllowedRecipients
SMTPAllowedRecipientsRegexp *regexp.Regexp
// SMTPIgnoreRejectedRecipients if true, will accept emails to rejected recipients with 2xx response but silently drop them
SMTPIgnoreRejectedRecipients bool
// POP3Listen address - if set then Mailpit will start the POP3 server and listen on this address
POP3Listen = "[::]:1110"
// POP3AuthFile for POP3 authentication
POP3AuthFile string
// POP3TLSCert TLS certificate
POP3TLSCert string
// POP3TLSKey TLS certificate key
POP3TLSKey string
// EnableSpamAssassin must be either <host>:<port> or "postmark"
EnableSpamAssassin string
// HideDeleteAllButton hides the delete all button in the web UI
HideDeleteAllButton bool
// WebhookURL for calling
WebhookURL string
// ContentSecurityPolicy for HTTP server - set via VerifyConfig()
ContentSecurityPolicy string
// AllowUntrustedTLS allows untrusted HTTPS connections link checking & screenshot generation
AllowUntrustedTLS bool
// PrometheusListen address for Prometheus metrics server
// Empty = disabled, true= use existing web server, address = separate server
PrometheusListen string
// ChaosTriggers are parsed and set in the chaos module
ChaosTriggers string
// DisableHTMLCheck DEPRECATED 2024/04/13 - kept here to display console warning only
DisableHTMLCheck = false
// DisableVersionCheck disables version checking
DisableVersionCheck bool
// DemoMode disables SMTP relay, link checking & HTTP send functionality
DemoMode = false
)
// AutoTag struct for auto-tagging
type autoTag struct {
Match string
Tags []string
}
// SMTPRelayConfigStruct struct for parsing yaml & storing variables
type SMTPRelayConfigStruct struct {
Host string `yaml:"host"` // SMTP host
Port int `yaml:"port"` // SMTP port
STARTTLS bool `yaml:"starttls"` // whether to use STARTTLS
TLS bool `yaml:"tls"` // whether to use TLS
AllowInsecure bool `yaml:"allow-insecure"` // allow insecure authentication, ignore TLS validation
Auth string `yaml:"auth"` // none, plain, login, 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"` // allow overriding the bounce address
OverrideFrom string `yaml:"override-from"` // allow overriding of the from address
AllowedRecipients string `yaml:"allowed-recipients"` // regex, if set needs to match for mails to be relayed
AllowedRecipientsRegexp *regexp.Regexp // compiled regexp using AllowedRecipients
BlockedRecipients string `yaml:"blocked-recipients"` // regex, if set prevents relating to these addresses
BlockedRecipientsRegexp *regexp.Regexp // compiled regexp using BlockedRecipients
PreserveMessageIDs bool `yaml:"preserve-message-ids"` // preserve the original Message-ID when relaying
ForwardSMTPErrors bool `yaml:"forward-smtp-errors"` // whether to log smtp-errors or forward them to upstream-client
// DEPRECATED 2024/03/12
RecipientAllowlist string `yaml:"recipient-allowlist"`
}
// SMTPForwardConfigStruct struct for parsing yaml & storing variables
type SMTPForwardConfigStruct struct {
To string `yaml:"to"` // comma-separated list of email addresses
Host string `yaml:"host"` // SMTP host
Port int `yaml:"port"` // SMTP port
STARTTLS bool `yaml:"starttls"` // whether to use STARTTLS
TLS bool `yaml:"tls"` // whether to use TLS
AllowInsecure bool `yaml:"allow-insecure"` // allow insecure authentication, ignore TLS validation
Auth string `yaml:"auth"` // none, plain, login, 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"` // allow overriding the bounce address
OverrideFrom string `yaml:"override-from"` // allow overriding of the from address
ForwardSMTPErrors bool `yaml:"forward-smtp-errors"` // whether to log smtp-errors or forward them to upstream-client
}
// VerifyConfig wil do some basic checking
func VerifyConfig() error {
cssFontRestriction := "*"
if BlockRemoteCSSAndFonts {
cssFontRestriction = "'self'"
}
// The default Content Security Policy is updates on every application page load to replace script-src 'self'
// with a random nonce ID to prevent XSS. This applies to the Mailpit app & API.
// See server.middleWareFunc()
ContentSecurityPolicy = fmt.Sprintf(
"default-src 'self'; script-src 'self'; style-src %s 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src %s data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';",
cssFontRestriction, cssFontRestriction,
)
if Database != "" && isDir(Database) {
Database = filepath.Join(Database, "mailpit.db")
}
if Compression < 0 || Compression > 3 {
return errors.New("[db] compression level must be between 0 and 3")
}
Label = tools.Normalize(Label)
if err := parseMaxAge(); err != nil {
return err
}
TenantID = DBTenantID(TenantID)
if TenantID != "" {
logger.Log().Infof("[db] using tenant \"%s\"", TenantID)
}
re := regexp.MustCompile(`.*:\d+$`)
if _, _, isSocket := tools.UnixSocket(SMTPListen); !isSocket && !re.MatchString(SMTPListen) {
return errors.New("[smtp] bind should be in the format of <ip>:<port>")
}
if _, _, isSocket := tools.UnixSocket(HTTPListen); !isSocket && !re.MatchString(HTTPListen) {
return errors.New("[ui] HTTP bind should be in the format of <ip>:<port>")
}
// Web UI & API
if UIAuthFile != "" {
UIAuthFile = filepath.Clean(UIAuthFile)
if !isFile(UIAuthFile) {
return fmt.Errorf("[ui] HTTP password file not found or readable: %s", UIAuthFile)
}
b, err := os.ReadFile(UIAuthFile)
if err != nil {
return err
}
if err := auth.SetUIAuth(string(b)); err != nil {
return err
}
}
if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" {
return errors.New("[ui] you must provide both a UI TLS certificate and a key")
}
if UITLSCert != "" {
if strings.HasPrefix(UITLSCert, "sans:") {
// generate a self-signed certificate
UITLSCert = snakeoil.Public(UITLSCert)
} else {
UITLSCert = filepath.Clean(UITLSCert)
}
if strings.HasPrefix(UITLSKey, "sans:") {
// generate a self-signed key
UITLSKey = snakeoil.Private(UITLSKey)
} else {
UITLSKey = filepath.Clean(UITLSKey)
}
if !isFile(UITLSCert) {
return fmt.Errorf("[ui] TLS certificate not found or readable: %s", UITLSCert)
}
if !isFile(UITLSKey) {
return fmt.Errorf("[ui] TLS key not found or readable: %s", UITLSKey)
}
}
// Send API
if SendAPIAuthFile != "" {
SendAPIAuthFile = filepath.Clean(SendAPIAuthFile)
if !isFile(SendAPIAuthFile) {
return fmt.Errorf("[send-api] password file not found or readable: %s", SendAPIAuthFile)
}
b, err := os.ReadFile(SendAPIAuthFile)
if err != nil {
return err
}
if err := auth.SetSendAPIAuth(string(b)); err != nil {
return err
}
logger.Log().Info("[send-api] enabling basic authentication")
}
if auth.SendAPICredentials != nil && SendAPIAuthAcceptAny {
return errors.New("[send-api] authentication cannot use both credentials and --send-api-auth-accept-any")
}
if SendAPIAuthAcceptAny && auth.UICredentials != nil {
logger.Log().Info("[send-api] disabling authentication")
}
// Prometheus configuration validation
if PrometheusListen != "" {
mode := strings.ToLower(strings.TrimSpace(PrometheusListen))
if mode != "true" && mode != "false" {
// Validate as address for separate server mode
_, err := net.ResolveTCPAddr("tcp", PrometheusListen)
if err != nil {
return fmt.Errorf("[prometheus] %s", err.Error())
}
} else if mode == "true" {
logger.Log().Info("[prometheus] enabling metrics")
}
}
// SMTP server
if SMTPTLSCert != "" && SMTPTLSKey == "" || SMTPTLSCert == "" && SMTPTLSKey != "" {
return errors.New("[smtp] you must provide both an SMTP TLS certificate and a key")
}
if SMTPTLSCert != "" {
if strings.HasPrefix(SMTPTLSCert, "sans:") {
// generate a self-signed certificate
SMTPTLSCert = snakeoil.Public(SMTPTLSCert)
} else {
SMTPTLSCert = filepath.Clean(SMTPTLSCert)
}
if strings.HasPrefix(SMTPTLSKey, "sans:") {
// generate a self-signed key
SMTPTLSKey = snakeoil.Private(SMTPTLSKey)
} else {
SMTPTLSKey = filepath.Clean(SMTPTLSKey)
}
if !isFile(SMTPTLSCert) {
return fmt.Errorf("[smtp] TLS certificate not found or readable: %s", SMTPTLSCert)
}
if !isFile(SMTPTLSKey) {
return fmt.Errorf("[smtp] TLS key not found or readable: %s", SMTPTLSKey)
}
} else if SMTPRequireTLS {
return errors.New("[smtp] TLS cannot be required without an SMTP TLS certificate and key")
} else if SMTPRequireSTARTTLS {
return errors.New("[smtp] STARTTLS cannot be required without an SMTP TLS certificate and key")
}
if SMTPRequireSTARTTLS && SMTPAuthAllowInsecure || SMTPRequireTLS && SMTPAuthAllowInsecure {
return errors.New("[smtp] TLS cannot be required with --smtp-auth-allow-insecure")
}
if SMTPRequireSTARTTLS && SMTPRequireTLS {
return errors.New("[smtp] TLS & STARTTLS cannot be required together")
}
if SMTPAuthFile != "" {
SMTPAuthFile = filepath.Clean(SMTPAuthFile)
if !isFile(SMTPAuthFile) {
return fmt.Errorf("[smtp] password file not found or readable: %s", SMTPAuthFile)
}
b, err := os.ReadFile(SMTPAuthFile)
if err != nil {
return err
}
if err := auth.SetSMTPAuth(string(b)); err != nil {
return err
}
if !SMTPAuthAllowInsecure {
// https://www.rfc-editor.org/rfc/rfc4954
// A server implementation MUST implement a configuration in which
// it does NOT permit any plaintext password mechanisms, unless either
// the STARTTLS [SMTP-TLS] command has been negotiated or some other
// mechanism that protects the session from password snooping has been
// provided. Server sites SHOULD NOT use any configuration which
// permits a plaintext password mechanism without such a protection
// mechanism against password snooping.
SMTPRequireSTARTTLS = true
}
}
if auth.SMTPCredentials != nil && SMTPAuthAcceptAny {
return errors.New("[smtp] authentication cannot use both credentials and --smtp-auth-accept-any")
}
if SMTPTLSCert == "" && (auth.SMTPCredentials != nil || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
return errors.New("[smtp] authentication requires STARTTLS or TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
}
if err := parseChaosTriggers(); err != nil {
return fmt.Errorf("[chaos] %s", err.Error())
}
if chaos.Enabled {
logger.Log().Info("[chaos] is enabled")
}
// POP3 server
if POP3TLSCert != "" {
if strings.HasPrefix(POP3TLSCert, "sans:") {
// generate a self-signed certificate
POP3TLSCert = snakeoil.Public(POP3TLSCert)
} else {
POP3TLSCert = filepath.Clean(POP3TLSCert)
}
if strings.HasPrefix(POP3TLSKey, "sans:") {
// generate a self-signed key
POP3TLSKey = snakeoil.Private(POP3TLSKey)
} else {
POP3TLSKey = filepath.Clean(POP3TLSKey)
}
if !isFile(POP3TLSCert) {
return fmt.Errorf("[pop3] TLS certificate not found or readable: %s", POP3TLSCert)
}
if !isFile(POP3TLSKey) {
return fmt.Errorf("[pop3] TLS key not found or readable: %s", POP3TLSKey)
}
}
if POP3TLSCert != "" && POP3TLSKey == "" || POP3TLSCert == "" && POP3TLSKey != "" {
return errors.New("[pop3] you must provide both a POP3 TLS certificate and a key")
}
if POP3Listen != "" {
_, err := net.ResolveTCPAddr("tcp", POP3Listen)
if err != nil {
return fmt.Errorf("[pop3] %s", err.Error())
}
}
if POP3AuthFile != "" {
POP3AuthFile = filepath.Clean(POP3AuthFile)
if !isFile(POP3AuthFile) {
return fmt.Errorf("[pop3] password file not found or readable: %s", POP3AuthFile)
}
b, err := os.ReadFile(POP3AuthFile)
if err != nil {
return err
}
if err := auth.SetPOP3Auth(string(b)); err != nil {
return err
}
}
// Web root
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
if WebhookURL != "" && !isValidURL(WebhookURL) {
return fmt.Errorf("webhook URL does not appear to be a valid URL (%s)", WebhookURL)
}
// DEPRECATED 2024/04/13
if DisableHTMLCheck {
logger.Log().Warn("--disable-html-check has been deprecated and is no longer used")
}
if EnableSpamAssassin != "" {
spamassassin.SetService(EnableSpamAssassin)
logger.Log().Infof("[spamassassin] enabled via %s", EnableSpamAssassin)
if err := spamassassin.Ping(); err != nil {
logger.Log().Warnf("[spamassassin] ping: %s", err.Error())
}
}
// load tag filters & options
TagFilters = []autoTag{}
if err := loadTagsFromArgs(CLITagsArg); err != nil {
return err
}
if err := loadTagsFromConfig(TagsConfig); err != nil {
return err
}
if err := parseTagsDisable(TagsDisable); err != nil {
return err
}
if SMTPAllowedRecipients != "" {
restrictRegexp, err := regexp.Compile(SMTPAllowedRecipients)
if err != nil {
return fmt.Errorf("[smtp] failed to compile smtp-allowed-recipients regexp: %s", err.Error())
}
SMTPAllowedRecipientsRegexp = restrictRegexp
logger.Log().Infof("[smtp] only allowing recipients matching regexp: %s", SMTPAllowedRecipients)
}
if SMTPIgnoreRejectedRecipients {
if SMTPAllowedRecipientsRegexp == nil {
logger.Log().Warn("[smtp] ignoring rejected recipients has no effect without setting smtp-allowed-recipients")
} else {
logger.Log().Info("[smtp] ignoring rejected recipients")
}
}
if err := parseRelayConfig(SMTPRelayConfigFile); err != nil {
return err
}
// separate relay config validation to account for environment variables
if err := validateRelayConfig(); err != nil {
return err
}
if !ReleaseEnabled && SMTPRelayAll || !ReleaseEnabled && SMTPRelayMatching != "" {
return errors.New("[relay] a relay configuration must be set to auto-relay any messages")
}
if SMTPRelayMatching != "" {
if SMTPRelayAll {
logger.Log().Warnf("[relay] ignoring smtp-relay-matching when smtp-relay-all is enabled")
} else {
re, err := regexp.Compile(SMTPRelayMatching)
if err != nil {
return fmt.Errorf("[relay] failed to compile smtp-relay-matching regexp: %s", err.Error())
}
SMTPRelayMatchingRegexp = re
logger.Log().Infof(
"[relay] auto-relaying new messages to recipients matching \"%s\" via %s:%d",
SMTPRelayMatching, SMTPRelayConfig.Host, SMTPRelayConfig.Port,
)
}
}
if SMTPRelayAll {
// this deserves a warning
logger.Log().Warnf("[relay] auto-relaying all new messages via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
}
if err := parseForwardConfig(SMTPForwardConfigFile); err != nil {
return err
}
// separate forwarding config validation to account for environment variables
if err := validateForwardConfig(); err != nil {
return err
}
if DemoMode {
MaxMessages = 1000
// this deserves a warning
logger.Log().Info("demo mode enabled")
}
return nil
}
================================================
FILE: config/tags.go
================================================
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/goccy/go-yaml"
)
var (
// TagsDisablePlus disables message tagging using plus-addresses (user+tag@example.com) - set via verifyConfig()
TagsDisablePlus bool
// TagsDisableXTags disables message tagging via the X-Tags header - set via verifyConfig()
TagsDisableXTags bool
)
type yamlTags struct {
Filters []yamlTag `yaml:"filters"`
}
type yamlTag struct {
Match string `yaml:"match"`
Tags string `yaml:"tags"`
}
// Load tags from a configuration from a file, if set
func loadTagsFromConfig(c string) error {
if c == "" {
return nil // not set, ignore
}
c = filepath.Clean(c)
if !isFile(c) {
return fmt.Errorf("[tags] configuration file not found or unreadable: %s", c)
}
data, err := os.ReadFile(c)
if err != nil {
return fmt.Errorf("[tags] %s", err.Error())
}
conf := yamlTags{}
if err := yaml.Unmarshal(data, &conf); err != nil {
return err
}
if conf.Filters == nil {
return fmt.Errorf("[tags] missing tag: array in %s", c)
}
for _, t := range conf.Filters {
tags := strings.Split(t.Tags, ",")
TagFilters = append(TagFilters, autoTag{Match: t.Match, Tags: tags})
}
logger.Log().Debugf("[tags] loaded %s from config %s", tools.Plural(len(conf.Filters), "tag filter", "tag filters"), c)
return nil
}
func loadTagsFromArgs(c string) error {
if c == "" {
return nil // not set, ignore
}
args := tools.ArgsParser(c)
for _, a := range args {
t := strings.Split(a, "=")
if len(t) > 1 {
match := strings.TrimSpace(strings.ToLower(strings.Join(t[1:], "=")))
tags := strings.Split(t[0], ",")
TagFilters = append(TagFilters, autoTag{Match: match, Tags: tags})
} else {
return fmt.Errorf("[tag] error parsing tags (%s)", a)
}
}
logger.Log().Debugf("[tags] loaded %s from CLI args", tools.Plural(len(args), "tag filter", "tag filters"))
return nil
}
func parseTagsDisable(s string) error {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
parts := strings.SplitSeq(strings.ToLower(s), ",")
for p := range parts {
switch strings.TrimSpace(p) {
case "x-tags", "xtags":
TagsDisableXTags = true
case "plus-addresses", "plus-addressing":
TagsDisablePlus = true
default:
return fmt.Errorf("[tags] invalid --tags-disable option: %s", p)
}
}
return nil
}
================================================
FILE: config/utils.go
================================================
package config
import (
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/axllent/mailpit/internal/tools"
)
// IsFile returns whether a file exists and is readable
func isFile(path string) bool {
f, err := os.Open(filepath.Clean(path))
defer func() { _ = f.Close() }()
return err == nil
}
// IsDir returns whether a path is a directory
func isDir(path string) bool {
info, err := os.Stat(path)
if err != nil || os.IsNotExist(err) || !info.IsDir() {
return false
}
return true
}
func isValidURL(s string) bool {
u, err := url.ParseRequestURI(s)
if err != nil {
return false
}
return strings.HasPrefix(u.Scheme, "http")
}
// DBTenantID converts a tenant ID to a DB-friendly value if set
func DBTenantID(s string) string {
s = tools.Normalize(s)
if s != "" {
re := regexp.MustCompile(`[^a-zA-Z0-9\_]`)
s = re.ReplaceAllString(s, "_")
if !strings.HasSuffix(s, "_") {
s = s + "_"
}
}
return s
}
================================================
FILE: config/validators.go
================================================
package config
import (
"errors"
"fmt"
"net/mail"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/smtpd/chaos"
"github.com/goccy/go-yaml"
)
// Parse the --max-age value (if set)
func parseMaxAge() error {
if MaxAge == "" {
return nil
}
re := regexp.MustCompile(`^\d+(h|d)$`)
if !re.MatchString(MaxAge) {
return fmt.Errorf("max-age must be either <int>h for hours or <int>d for days: %s", MaxAge)
}
if before, ok := strings.CutSuffix(MaxAge, "h"); ok {
hours, err := strconv.Atoi(before)
if err != nil {
return err
}
MaxAgeInHours = hours
return nil
}
days, err := strconv.Atoi(strings.TrimSuffix(MaxAge, "d"))
if err != nil {
return err
}
logger.Log().Debugf("[db] auto-deleting messages older than %s", MaxAge)
MaxAgeInHours = days * 24
return nil
}
// Parse the SMTPRelayConfigFile (if set)
func parseRelayConfig(c string) error {
if c == "" {
return nil
}
c = filepath.Clean(c)
if !isFile(c) {
return fmt.Errorf("[relay] configuration not found or readable: %s", c)
}
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("[relay] host not set")
}
// DEPRECATED 2024/03/12
if SMTPRelayConfig.RecipientAllowlist != "" {
logger.Log().Warn("[relay] 'recipient-allowlist' is deprecated, use 'allowed-recipients' instead")
if SMTPRelayConfig.AllowedRecipients == "" {
SMTPRelayConfig.AllowedRecipients = SMTPRelayConfig.RecipientAllowlist
}
}
return nil
}
// Validate the SMTPRelayConfig (if Host is set)
func validateRelayConfig() error {
if SMTPRelayConfig.Host == "" {
return nil
}
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("[relay] host username or password not set for PLAIN authentication")
}
} else if SMTPRelayConfig.Auth == "login" {
SMTPRelayConfig.Auth = "login"
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
return fmt.Errorf("[relay] host username or password not set for LOGIN authentication")
}
} else if strings.HasPrefix(SMTPRelayConfig.Auth, "cram") {
SMTPRelayConfig.Auth = "cram-md5"
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Secret == "" {
return fmt.Errorf("[relay] host username or secret not set for CRAM-MD5 authentication")
}
} else {
return fmt.Errorf("[relay] authentication method not supported: %s", SMTPRelayConfig.Auth)
}
if SMTPRelayConfig.AllowedRecipients != "" {
re, err := regexp.Compile(SMTPRelayConfig.AllowedRecipients)
if err != nil {
return fmt.Errorf("[relay] failed to compile recipient allowlist regexp: %s", err.Error())
}
SMTPRelayConfig.AllowedRecipientsRegexp = re
logger.Log().Infof("[relay] recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.AllowedRecipients)
}
if SMTPRelayConfig.BlockedRecipients != "" {
re, err := regexp.Compile(SMTPRelayConfig.BlockedRecipients)
if err != nil {
return fmt.Errorf("[relay] failed to compile recipient blocklist regexp: %s", err.Error())
}
SMTPRelayConfig.BlockedRecipientsRegexp = re
logger.Log().Infof("[relay] recipient blocklist is active with the following regexp: %s", SMTPRelayConfig.BlockedRecipients)
}
if SMTPRelayConfig.OverrideFrom != "" {
m, err := mail.ParseAddress(SMTPRelayConfig.OverrideFrom)
if err != nil {
return fmt.Errorf("[relay] override-from is not a valid email address: %s", SMTPRelayConfig.OverrideFrom)
}
SMTPRelayConfig.OverrideFrom = m.Address
}
if SMTPRelayConfig.STARTTLS && SMTPRelayConfig.TLS {
return fmt.Errorf("[relay] TLS & STARTTLS cannot be required together")
}
ReleaseEnabled = true
logger.Log().Infof("[relay] enabling message relaying via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
return nil
}
// Parse the SMTPForwardConfigFile (if set)
func parseForwardConfig(c string) error {
if c == "" {
return nil
}
c = filepath.Clean(c)
if !isFile(c) {
return fmt.Errorf("[forward] configuration not found or readable: %s", c)
}
data, err := os.ReadFile(c)
if err != nil {
return err
}
if err := yaml.Unmarshal(data, &SMTPForwardConfig); err != nil {
return err
}
if SMTPForwardConfig.Host == "" {
return errors.New("[forward] host not set")
}
return nil
}
// Validate the SMTPForwardConfig (if Host is set)
func validateForwardConfig() error {
if SMTPForwardConfig.Host == "" {
return nil
}
if SMTPForwardConfig.Port == 0 {
SMTPForwardConfig.Port = 25 // default
}
SMTPForwardConfig.Auth = strings.ToLower(SMTPForwardConfig.Auth)
if SMTPForwardConfig.Auth == "" || SMTPForwardConfig.Auth == "none" || SMTPForwardConfig.Auth == "false" {
SMTPForwardConfig.Auth = "none"
} else if SMTPForwardConfig.Auth == "plain" {
if SMTPForwardConfig.Username == "" || SMTPForwardConfig.Password == "" {
return fmt.Errorf("[forward] host username or password not set for PLAIN authentication")
}
} else if SMTPForwardConfig.Auth == "login" {
SMTPForwardConfig.Auth = "login"
if SMTPForwardConfig.Username == "" || SMTPForwardConfig.Password == "" {
return fmt.Errorf("[forward] host username or password not set for LOGIN authentication")
}
} else if strings.HasPrefix(SMTPForwardConfig.Auth, "cram") {
SMTPForwardConfig.Auth = "cram-md5"
if SMTPForwardConfig.Username == "" || SMTPForwardConfig.Secret == "" {
return fmt.Errorf("[forward] host username or secret not set for CRAM-MD5 authentication")
}
} else {
return fmt.Errorf("[forward] authentication method not supported: %s", SMTPForwardConfig.Auth)
}
if SMTPForwardConfig.To == "" {
return errors.New("[forward] To addresses missing")
}
to := []string{}
addresses := strings.SplitSeq(SMTPForwardConfig.To, ",")
for a := range addresses {
a = strings.TrimSpace(a)
m, err := mail.ParseAddress(a)
if err != nil {
return fmt.Errorf("[forward] To address is not a valid email address: %s", a)
}
to = append(to, m.Address)
}
if len(to) == 0 {
return errors.New("[forward] no valid To addresses found")
}
// overwrite the To field with the cleaned up list
SMTPForwardConfig.To = strings.Join(to, ",")
if SMTPForwardConfig.OverrideFrom != "" {
m, err := mail.ParseAddress(SMTPForwardConfig.OverrideFrom)
if err != nil {
return fmt.Errorf("[forward] override-from is not a valid email address: %s", SMTPForwardConfig.OverrideFrom)
}
SMTPForwardConfig.OverrideFrom = m.Address
}
if SMTPForwardConfig.STARTTLS && SMTPForwardConfig.TLS {
return fmt.Errorf("[forward] TLS & STARTTLS cannot be required together")
}
logger.Log().Infof("[forward] enabling message forwarding to %s via %s:%d", SMTPForwardConfig.To, SMTPForwardConfig.Host, SMTPForwardConfig.Port)
return nil
}
func parseChaosTriggers() error {
if ChaosTriggers == "" {
return nil
}
re := regexp.MustCompile(`^([a-zA-Z0-0]+):(\d\d\d):(\d+(\.\d)?)$`)
parts := strings.SplitSeq(ChaosTriggers, ",")
for p := range parts {
p = strings.TrimSpace(p)
if !re.MatchString(p) {
return fmt.Errorf("invalid argument: %s", p)
}
matches := re.FindAllStringSubmatch(p, 1)
key := matches[0][1]
errorCode, err := strconv.Atoi(matches[0][2])
if err != nil {
return err
}
probability, err := strconv.Atoi(matches[0][3])
if err != nil {
return err
}
if err := chaos.Set(key, errorCode, probability); err != nil {
return err
}
}
return nil
}
================================================
FILE: esbuild.config.mjs
================================================
import * as esbuild from "esbuild";
import pluginVue from "esbuild-plugin-vue-next";
import { sassPlugin } from "esbuild-sass-plugin";
const doWatch = process.env.WATCH === "true";
const doMinify = process.env.MINIFY === "true";
const ctx = await esbuild.context({
entryPoints: ["server/ui-src/app.js", "server/ui-src/docs.js"],
bundle: true,
minify: doMinify,
sourcemap: false,
define: {
__VUE_OPTIONS_API__: "true",
__VUE_PROD_DEVTOOLS__: "false",
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false",
},
outdir: "server/ui/dist/",
plugins: [
pluginVue(),
sassPlugin({
silenceDeprecations: ["import"],
quietDeps: true,
}),
],
loader: {
".svg": "file",
".woff": "file",
".woff2": "file",
},
logLevel: "info",
});
if (doWatch) {
await ctx.watch();
} else {
await ctx.rebuild();
ctx.dispose();
}
================================================
FILE: eslint.config.js
================================================
import eslintConfigPrettier from "eslint-config-prettier/flat";
import globals from "globals";
import { includeIgnoreFile } from "@eslint/compat";
import js from "@eslint/js";
import vue from "eslint-plugin-vue";
import { fileURLToPath } from "node:url";
const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url));
export default [
/* Use .gitignore to prevent linting of irrelevant files */
includeIgnoreFile(gitignorePath, ".gitignore"),
/* ESLint's recommended rules */
{
files: ["**/*.js", "**/*.vue"],
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: js.configs.recommended.rules,
},
/* Vue-specific rules */
...vue.configs["flat/recommended"],
/* Prettier is responsible for formatting, so we disable conflicting rules */
eslintConfigPrettier,
/* Our custom rules */
{
rules: {
/* Always use arrow functions for tidiness and consistency */
"prefer-arrow-callback": "error",
/* Always use camelCase for variable names */
camelcase: [
"error",
{
ignoreDestructuring: false,
ignoreGlobals: true,
ignoreImports: false,
properties: "never",
},
],
/* The default case in switch statements must always be last */
"default-case-last": "error",
/* Always use dot notation where possible (e.g. `obj.val` over `obj['val']`) */
"dot-notation": "error",
/* Always use `===` and `!==` for comparisons unless unambiguous */
eqeqeq: ["error", "smart"],
/* Never use `eval()` as it violates our CSP and can lead to security issues */
"no-eval": "error",
"no-implied-eval": "error",
/* Prevents accidental use of template literals in plain strings, e.g. "my ${var}" */
"no-template-curly-in-string": "error",
/* Avoid unnecessary ternary operators */
"no-unneeded-ternary": "error",
/* Avoid unused expressions that have no purpose */
"no-unused-expressions": "error",
/* Always use `const` or `let` to make scope behaviour clear */
"no-var": "error",
/* Always use shorthand syntax for objects where possible, e.g. { a, b() { } } */
"object-shorthand": "error",
/* Always use `const` for variables that are never reassigned */
"prefer-const": "error",
},
},
];
================================================
FILE: go.mod
================================================
module github.com/axllent/mailpit
go 1.25.0
require (
github.com/PuerkitoBio/goquery v1.11.0
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/axllent/ghru/v2 v2.1.0
github.com/axllent/semver v1.0.0
github.com/goccy/go-yaml v1.19.2
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/jhillyerd/enmime/v2 v2.3.0
github.com/klauspost/compress v1.18.4
github.com/kovidgoyal/imaging v1.8.20
github.com/leporo/sqlf v1.4.0
github.com/lithammer/shortuuid/v4 v4.2.0
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/tg123/go-htpasswd v1.2.4
github.com/vanng822/go-premailer v1.32.0
golang.org/x/crypto v0.48.0
golang.org/x/net v0.51.0
golang.org/x/text v0.34.0
golang.org/x/time v0.15.0
modernc.org/sqlite v1.46.1
)
require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inbucket/html2text v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
github.com/kovidgoyal/go-shm v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.7 // indirect
github.com/olekukonko/tablewriter v1.1.3 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/reiver/go-oi v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vanng822/css v1.0.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/image v0.36.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.42.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
================================================
FILE: go.sum
================================================
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/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/axllent/ghru/v2 v2.1.0 h1:zNW96KO+rmXggizZhHzIX7MExOiV4jx+63Y9nXlwLV0=
github.com/axllent/ghru/v2 v2.1.0/go.mod h1:8l7s1phdc375vvf8LHxT7wnJqXlThdHJR5EBtHNWhTg=
github.com/axllent/semver v1.0.0 h1:FDekA0alnMed5bWVWjUwBS+6QouZZkmPXsGVmOfjWOg=
github.com/axllent/semver v1.0.0/go.mod h1:ySHHYLyFX3vKAALmaO8TOOJkzGRsUNmzFiIWwPm8li8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
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/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc=
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs=
github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jhillyerd/enmime/v2 v2.3.0 h1:Y/pzQanyU8nkSgB2npXX8Dha5OItJE/QwbDJM4sf/kU=
github.com/jhillyerd/enmime/v2 v2.3.0/go.mod h1:mGKXAP45l6pF6HZiaLhgSYsgteJskaSIYmEZXpw6ZpI=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
github.com/kovidgoyal/imaging v1.8.20 h1:74GZ7C2rIm3rqmGEjK1GvvPOOnJ0SS5iDOa6Flfo0b0=
github.com/kovidgoyal/imaging v1.8.20/go.mod h1:d3phGYkTChGYkY4y++IjpHgUGhWGELDc2NEQAqxwZZg=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
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/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
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/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62 h1:XMG5DklHoioVYysfYglOB7vRBg/LOUJZy2mq2QyedLg=
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62/go.mod h1:niAM5cni0I/47IFA995xQfeK58Mkbb7FHJjacY4OGQg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4=
github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8 h1:BoxiqWvhprOB2isgM59s8wkgKwAoyQH66Twfmof41oE=
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/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.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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU=
github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0=
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/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
github.com/vanng822/go-premailer v1.32.0 h1:dW1y2IKSBQyYIwMc9comDA2e+00/pJ1kVXf3v4sqAJo=
github.com/vanng822/go-premailer v1.32.0/go.mod h1:4gVC6Hs+ESjSSfB1ohMwLqwuGoJ76cc0c2VM7DYqr0s=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
================================================
FILE: install.sh
================================================
#!/bin/sh
# This script will install the latest release of Mailpit.
# Check dependencies is installed
for cmd in curl tar; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Then $cmd command is required but not installed."
echo "Please install $cmd and try again."
exit 1
fi
done
# Check if the OS is supported.
OS=
case "$(uname -s)" in
Linux) OS="linux" ;;
Darwin) OS="darwin" ;;
*)
echo "OS not supported."
exit 2
;;
esac
# Detect the architecture of the OS.
OS_ARCH=
case "$(uname -m)" in
x86_64 | amd64)
OS_ARCH="amd64"
;;
i?86 | x86)
OS_ARCH="386"
;;
aarch64 | arm64)
OS_ARCH="arm64"
;;
*)
echo "OS architecture not supported."
exit 2
;;
esac
GH_REPO="axllent/mailpit"
INSTALL_PATH="${INSTALL_PATH:-/usr/local/bin}"
TIMEOUT=90
# This is used to authenticate with the GitHub API. (Fix the public rate limiting issue)
# Try the GITHUB_TOKEN environment variable is set globally.
GITHUB_API_TOKEN="${GITHUB_TOKEN:-}"
# Update the default values if the user has set.
while [ $# -gt 0 ]; do
case $1 in
--install-path)
shift
case "$1" in
*/*)
# Remove trailing slashes from the path.
INSTALL_PATH="$(echo "$1" | sed 's#/\+$##')"
[ -z "$INSTALL_PATH" ] && INSTALL_PATH="/"
;;
esac
;;
--auth | --auth-token | --github-token | --token)
shift
case "$1" in
gh*)
GITHUB_API_TOKEN="$1"
;;
esac
;;
*) ;;
esac
shift
done
# Description of the sort parameters for curl command.
# -s: Silent mode.
# -f: Fail silently on server errors.
# -L: Follow redirects.
# -m: Set maximum time allowed for the transfer.
if [ -n "$GITHUB_API_TOKEN" ] && [ "${#GITHUB_API_TOKEN}" -gt 36 ]; then
CURL_OUTPUT="$(curl -sfL -m $TIMEOUT -H "Authorization: Bearer $GITHUB_API_TOKEN" https://api.github.com/repos/${GH_REPO}/releases/latest)"
EXIT_CODE=$?
else
CURL_OUTPUT="$(curl -sfL -m $TIMEOUT https://api.github.com/repos/${GH_REPO}/releases/latest)"
EXIT_CODE=$?
fi
VERSION=""
if [ $EXIT_CODE -eq 0 ]; then
# Extracts the latest version using jq, awk, or sed.
if command -v jq >/dev/null 2>&1; then
# Use jq -n because the output is not a valid JSON in sh.
VERSION=$(jq -n "$CURL_OUTPUT" | jq -r '.tag_name')
elif command -v awk >/dev/null 2>&1; then
VERSION=$(echo "$CURL_OUTPUT" | awk -F: '$1 ~ /tag_name/ {gsub(/[^v0-9\.]+/, "", $2) ;print $2; exit}')
elif command -v sed >/dev/null 2>&1; then
VERSION=$(echo "$CURL_OUTPUT" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p')
else
EXIT_CODE=3
fi
fi
# Validate the version.
case "$VERSION" in
v[0-9][0-9\.]*) ;;
*)
echo "There was an error trying to check what is the latest version of Mailpit."
echo "Please try again later."
exit $EXIT_CODE
;;
esac
TEMP_DIR="$(mktemp -qd)"
EXIT_CODE=$?
# Ensure the temporary directory exists and is a directory.
if [ -z "$TEMP_DIR" ] || [ ! -d "$TEMP_DIR" ]; then
echo "ERROR: Creating temporary directory."
exit $EXIT_CODE
fi
GH_REPO_BIN="mailpit-${OS}-${OS_ARCH}.tar.gz"
if [ "$INSTALL_PATH" = "/" ]; then
INSTALL_BIN_PATH="/mailpit"
else
INSTALL_BIN_PATH="${INSTALL_PATH}/mailpit"
fi
cd "$TEMP_DIR" || EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
# Download the latest release.
#
# Description of the sort parameters for curl command.
# -s: Silent mode.
# -f: Fail silently on server errors.
# -L: Follow redirects.
# -m: Set maximum time allowed for the transfer.
# -o: Write output to a file instead of stdout.
curl -sfL -m $TIMEOUT -o "${GH_REPO_BIN}" "https://github.com/${GH_REPO}/releases/download/${VERSION}/${GH_REPO_BIN}"
EXIT_CODE=$?
# The following conditions check each step of the installation.
# If there is an error in any of the steps, an error message is printed.
if [ $EXIT_CODE -eq 0 ]; then
if ! [ -f "${GH_REPO_BIN}" ]; then
EXIT_CODE=1
echo "ERROR: Downloading latest release."
fi
fi
if [ $EXIT_CODE -eq 0 ]; then
tar zxf "$GH_REPO_BIN"
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "ERROR: Extracting \"${GH_REPO_BIN}\"."
fi
fi
if [ $EXIT_CODE -eq 0 ] && [ ! -d "$INSTALL_PATH" ]; then
mkdir -p "${INSTALL_PATH}"
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "ERROR: Creating \"${INSTALL_PATH}\" directory."
fi
fi
if [ $EXIT_CODE -eq 0 ]; then
cp mailpit "$INSTALL_BIN_PATH"
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "ERROR: Copying mailpit to \"${INSTALL_PATH}\" directory."
fi
fi
if [ $EXIT_CODE -eq 0 ]; then
chmod 755 "$INSTALL_BIN_PATH"
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "ERROR: Setting permissions for \"$INSTALL_BIN_PATH\" binary."
fi
fi
# Set the owner and group to root:root if the script is run as root.
if [ $EXIT_CODE -eq 0 ] && [ "$(id -u)" -eq "0" ]; then
OWNER="root"
GROUP="root"
# Set the OWNER, GROUP variable when the OS not use the default root:root.
case "$OS" in
darwin) GROUP="wheel" ;;
*) ;;
esac
chown "${OWNER}:${GROUP}" "$INSTALL_BIN_PATH"
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "ERROR: Setting ownership for \"$INSTALL_BIN_PATH\" binary."
fi
fi
else
echo "ERROR: Changing to temporary directory."
exit $EXIT_CODE
fi
# Cleanup the temporary directory.
rm -rf "$TEMP_DIR"
# Check the EXIT_CODE variable, and print the success or error message.
if [ $EXIT_CODE -ne 0 ]; then
echo "There was an error installing Mailpit."
exit $EXIT_CODE
fi
echo "Installed successfully to \"$INSTALL_BIN_PATH\"."
exit 0
================================================
FILE: internal/auth/auth.go
================================================
// Package auth handles the web UI and SMTP authentication
package auth
import (
"regexp"
"strings"
"github.com/tg123/go-htpasswd"
)
var (
// UICredentials passwords
UICredentials *htpasswd.File
// SendAPICredentials passwords
SendAPICredentials *htpasswd.File
// SMTPCredentials passwords
SMTPCredentials *htpasswd.File
// POP3Credentials passwords
POP3Credentials *htpasswd.File
)
// SetUIAuth will set Basic Auth credentials required for the UI & API
func SetUIAuth(s string) error {
var err error
credentials := credentialsFromString(s)
if len(credentials) == 0 {
return nil
}
r := strings.NewReader(strings.Join(credentials, "\n"))
UICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
if err != nil {
return err
}
return nil
}
// SetSendAPIAuth will set Send API credentials
func SetSendAPIAuth(s string) error {
var err error
credentials := credentialsFromString(s)
if len(credentials) == 0 {
return nil
}
r := strings.NewReader(strings.Join(credentials, "\n"))
SendAPICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
if err != nil {
return err
}
return nil
}
// SetSMTPAuth will set SMTP credentials
func SetSMTPAuth(s string) error {
var err error
credentials := credentialsFromString(s)
if len(credentials) == 0 {
return nil
}
r := strings.NewReader(strings.Join(credentials, "\n"))
SMTPCredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
if err != nil {
return err
}
return nil
}
// SetPOP3Auth will set POP3 server credentials
func SetPOP3Auth(s string) error {
var err error
credentials := credentialsFromString(s)
if len(credentials) == 0 {
return nil
}
r := strings.NewReader(strings.Join(credentials, "\n"))
POP3Credentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
if err != nil {
return err
}
return nil
}
func credentialsFromString(s string) []string {
// split string by any whitespace character
re := regexp.MustCompile(`\s+`)
words := re.Split(s, -1)
credentials := []string{}
for _, w := range words {
if w != "" {
credentials = append(credentials, w)
}
}
return credentials
}
================================================
FILE: internal/dump/dump.go
================================================
// Package dump is used to export all messages from mailpit into a directory
package dump
import (
"encoding/json"
"errors"
"io"
"net/http"
"os"
"path"
"regexp"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server/apiv1"
)
var (
linkRe = regexp.MustCompile(`(?i)^https?:\/\/`)
outDir string
// Base URL of mailpit instance
base string
// URL is the base URL of a remove Mailpit instance
URL string
summary = []storage.MessageSummary{}
)
// Sync will sync all messages from the specified database or API to the specified output directory
func Sync(d string) error {
outDir = path.Clean(d)
if URL != "" {
if !linkRe.MatchString(URL) {
return errors.New("invalid URL")
}
base = strings.TrimRight(URL, "/") + "/"
}
if base == "" && config.Database == "" {
return errors.New("no database or API URL specified")
}
if !tools.IsDir(outDir) {
if err := os.MkdirAll(outDir, 0755); /* #nosec */ err != nil {
return err
}
}
if err := loadIDs(); err != nil {
return err
}
if err := saveMessages(); err != nil {
return err
}
return nil
}
// LoadIDs will load all message IDs from the specified database or API
func loadIDs() error {
if base != "" {
// remote
logger.Log().Debugf("Fetching messages summary from %s", base)
res, err := http.Get(base + "api/v1/messages?limit=0")
if err != nil {
return err
}
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var data apiv1.MessagesSummary
if err := json.Unmarshal(body, &data); err != nil {
return err
}
summary = data.Messages
} else {
// make sure the database isn't pruned while open
config.MaxMessages = 0
var err error
// local database
if err = storage.InitDB(); err != nil {
return err
}
logger.Log().Debugf("Fetching messages summary from %s", config.Database)
summary, err = storage.List(0, 0, 0)
if err != nil {
return err
}
}
if len(summary) == 0 {
return errors.New("no messages found")
}
return nil
}
func saveMessages() error {
for _, m := range summary {
out := path.Join(outDir, m.ID+".eml")
// skip if message exists
if tools.IsFile(out) {
continue
}
var b []byte
if base != "" {
res, err := http.Get(base + "api/v1/message/" + m.ID + "/raw")
if err != nil {
logger.Log().Errorf("error fetching message %s: %s", m.ID, err.Error())
continue
}
b, err = io.ReadAll(res.Body)
if err != nil {
logger.Log().Errorf("error fetching message %s: %s", m.ID, err.Error())
continue
}
} else {
var err error
b, err = storage.GetMessageRaw(m.ID)
if err != nil {
logger.Log().Errorf("error fetching message %s: %s", m.ID, err.Error())
continue
}
}
if err := os.WriteFile(out, b, 0644); /* #nosec */ err != nil {
logger.Log().Errorf("error writing message %s: %s", m.ID, err.Error())
continue
}
_ = os.Chtimes(out, m.Created, m.Created)
logger.Log().Debugf("Saved message %s to %s", m.ID, out)
}
return nil
}
================================================
FILE: internal/html2text/html2text.go
================================================
// Package html2text is a simple library to convert HTML to plain text
package html2text
import (
"bytes"
"log"
"regexp"
"strings"
"unicode"
"golang.org/x/net/html"
)
var (
re = regexp.MustCompile(`\s+`)
spaceRe = regexp.MustCompile(`(?mi)<\/(div|p|td|th|h[1-6]|ul|ol|li|address|article|aside|blockquote|dl|dt|footer|header|hr|main|nav|pre|table|thead|tfoot|video)><`)
brRe = regexp.MustCompile(`(?mi)<(br /|br)>`)
imgRe = regexp.MustCompile(`(?mi)<(img)`)
skip = make(map[string]bool)
)
func init() {
skip["script"] = true
skip["title"] = true
skip["head"] = true
skip["link"] = true
skip["meta"] = true
skip["style"] = true
skip["noscript"] = true
}
// Strip will convert a HTML string to plain text
func Strip(h string, includeLinks bool) string {
h = spaceRe.ReplaceAllString(h, "</$1> <")
h = brRe.ReplaceAllString(h, " ")
h = imgRe.ReplaceAllString(h, " <$1")
var buffer bytes.Buffer
doc, err := html.Parse(strings.NewReader(h))
if err != nil {
log.Fatal(err)
}
extract(doc, &buffer, includeLinks)
return clean(buffer.String())
}
func extract(node *html.Node, buff *bytes.Buffer, includeLinks bool) {
if node.Type == html.TextNode {
data := node.Data
if data != "" {
buff.WriteString(data)
}
}
for c := node.FirstChild; c != nil; c = c.NextSibling {
if _, skip := skip[c.Data]; !skip {
if includeLinks && c.Data == "a" {
for _, a := range c.Attr {
if a.Key == "href" && strings.HasPrefix(strings.ToLower(a.Val), "http") {
buff.WriteString(" " + a.Val + " ")
}
}
}
extract(c, buff, includeLinks)
}
}
}
func clean(text string) string {
// replace \uFEFF with space, see https://github.com/golang/go/issues/42274#issuecomment-1017258184
text = strings.ReplaceAll(text, string('\uFEFF'), " ")
// remove non-printable characters
text = strings.Map(func(r rune) rune {
if unicode.IsPrint(r) {
return r
}
return []rune(" ")[0]
}, text)
text = re.ReplaceAllString(text, " ")
return strings.TrimSpace(text)
}
================================================
FILE: internal/html2text/html2text_test.go
================================================
package html2text
import "testing"
func TestPlain(t *testing.T) {
tests := map[string]string{}
tests["this is a test"] = "this is a test"
tests["thiS IS a Test"] = "thiS IS a Test"
tests["thiS IS a Test :-)"] = "thiS IS a Test :-)"
tests["<h1>This is a test.</h1> "] = "This is a test."
tests["<p>Paragraph 1</p><p>Paragraph 2</p>"] = "Paragraph 1 Paragraph 2"
tests["<h1>Heading</h1><p>Paragraph</p>"] = "Heading Paragraph"
tests["<span>Alpha</span>bet <strong>chars</strong>"] = "Alphabet chars"
tests["<span><b>A</b>lpha</span>bet chars."] = "Alphabet chars."
tests["<table><tr><td>First</td><td>Second</td></table>"] = "First Second"
tests[`<h1>Heading</h1>
<p>Paragraph</p>`] = "Heading Paragraph"
tests[`<h1>Heading</h1><p> <a href="https://github.com">linked text</a></p>`] = "Heading linked text"
// broken html
tests[`<h1>Heading</h3><p> <a href="https://github.com">linked text.`] = "Heading linked text."
for str, expected := range tests {
res := Strip(str, false)
if res != expected {
t.Log("error:", res, "!=", expected)
t.Fail()
}
}
}
func TestWithLinks(t *testing.T) {
tests := map[string]string{}
tests["this is a test"] = "this is a test"
tests["thiS IS a Test"] = "thiS IS a Test"
tests["thiS IS a Test :-)"] = "thiS IS a Test :-)"
tests["<h1>This is a test.</h1> "] = "This is a test."
tests["<p>Paragraph 1</p><p>Paragraph 2</p>"] = "Paragraph 1 Paragraph 2"
tests["<h1>Heading</h1><p>Paragraph</p>"] = "Heading Paragraph"
tests["<span>Alpha</span>bet <strong>chars</strong>"] = "Alphabet chars"
tests["<span><b>A</b>lpha</span>bet chars."] = "Alphabet chars."
tests["<table><tr><td>First</td><td>Second</td></table>"] = "First Second"
tests["<h1>Heading</h1><p>Paragraph</p>"] = "Heading Paragraph"
tests[`<h1>Heading</h1>
<p>Paragraph</p>`] = "Heading Paragraph"
tests[`<h1>Heading</h1><p> <a href="https://github.com">linked text</a></p>`] = "Heading https://github.com linked text"
// broken html
tests[`<h1>Heading</h3><p> <a href="https://github.com">linked text.`] = "Heading https://github.com linked text."
for str, expected := range tests {
res := Strip(str, true)
if res != expected {
t.Log("error:", res, "!=", expected)
t.Fail()
}
}
}
func BenchmarkPlain(b *testing.B) {
for i := 0; i < b.N; i++ {
Strip(htmlTestData, false)
}
}
func BenchmarkLinks(b *testing.B) {
for i := 0; i < b.N; i++ {
Strip(htmlTestData, true)
}
}
var htmlTestData = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" style="font-family: sans-serif; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; box-sizing: border-box;" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<title>[axllent/mailpit] Run failed: .github/workflows/tests.yml - feature/swagger (284335a)</title>
</head>
<body style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; font-size: 14px; line-height: 1.5; color: #24292e; background-color: #fff; margin: 0;" bgcolor="#fff">
<table align="center" class="container-sm width-full" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; max-width: 544px; margin-right: auto; margin-left: auto; width: 100% !important; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important;">
<td class="center p-3" align="center" valign="top" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important; padding: 16px;">
<center style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important;">
<table border="0" cellspacing="0" cellpadding="0" align="center" class="width-full container-md" width="100%" style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; max-width: 768px; margin-right: auto; margin-left: auto; width: 100% !important; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important;">
<td align="center" style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important; padding: 0;">
<table style="box-sizing: border-box; border-spacing: 0; border-collapse: collapse; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important;">
<tbody style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" !important;">
<tr style="box-sizing: border-box; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe
gitextract_6s7dfcpy/
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ ├── SECURITY.md
│ ├── cliff.toml
│ ├── dependabot.yml
│ └── workflows/
│ ├── build-docker-edge.yml
│ ├── build-docker.yml
│ ├── close-stale-issues.yml
│ ├── codeql-analysis.yml
│ ├── release-build.yml
│ ├── tests-rqlite.yml
│ └── tests.yml
├── .gitignore
├── .prettierignore
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── cmd/
│ ├── dump.go
│ ├── ingest.go
│ ├── readyz.go
│ ├── reindex.go
│ ├── root.go
│ ├── sendmail.go
│ └── version.go
├── config/
│ ├── config.go
│ ├── tags.go
│ ├── utils.go
│ └── validators.go
├── esbuild.config.mjs
├── eslint.config.js
├── go.mod
├── go.sum
├── install.sh
├── internal/
│ ├── auth/
│ │ └── auth.go
│ ├── dump/
│ │ └── dump.go
│ ├── html2text/
│ │ ├── html2text.go
│ │ └── html2text_test.go
│ ├── htmlcheck/
│ │ ├── README.md
│ │ ├── caniemail-data.json
│ │ ├── caniemail.go
│ │ ├── config.go
│ │ ├── css.go
│ │ ├── html.go
│ │ ├── inline_test.go
│ │ ├── main.go
│ │ ├── platforms.go
│ │ └── structs.go
│ ├── linkcheck/
│ │ ├── linkcheck_test.go
│ │ ├── main.go
│ │ ├── status.go
│ │ └── structs.go
│ ├── logger/
│ │ └── logger.go
│ ├── pop3/
│ │ ├── functions.go
│ │ ├── pop3_test.go
│ │ └── server.go
│ ├── pop3client/
│ │ └── client.go
│ ├── prometheus/
│ │ └── metrics.go
│ ├── smtpd/
│ │ ├── chaos/
│ │ │ └── chaos.go
│ │ ├── forward.go
│ │ ├── main.go
│ │ ├── relay.go
│ │ ├── smtpd.go
│ │ └── smtpd_test.go
│ ├── snakeoil/
│ │ └── snakeoil.go
│ ├── spamassassin/
│ │ ├── postmark/
│ │ │ └── postmark.go
│ │ ├── spamassassin.go
│ │ └── spamc/
│ │ └── spamc.go
│ ├── stats/
│ │ └── stats.go
│ ├── storage/
│ │ ├── cron.go
│ │ ├── database.go
│ │ ├── functions_test.go
│ │ ├── messages.go
│ │ ├── messages_test.go
│ │ ├── notifications.go
│ │ ├── reindex.go
│ │ ├── schemas/
│ │ │ ├── 1.0.0.sql
│ │ │ ├── 1.1.0.sql
│ │ │ ├── 1.2.0.sql
│ │ │ ├── 1.21.2.sql
│ │ │ ├── 1.21.8.sql
│ │ │ ├── 1.23.0.sql
│ │ │ ├── 1.3.0.sql
│ │ │ ├── 1.4.0.sql
│ │ │ ├── 1.5.0.sql
│ │ │ └── README.md
│ │ ├── schemas.go
│ │ ├── search.go
│ │ ├── search_test.go
│ │ ├── settings.go
│ │ ├── structs.go
│ │ ├── tagfilters.go
│ │ ├── tags.go
│ │ ├── tags_test.go
│ │ ├── testdata/
│ │ │ ├── inline-attachment.eml
│ │ │ ├── mime-attachment.eml
│ │ │ ├── mixed-attachment.eml
│ │ │ ├── plain-text.eml
│ │ │ ├── regular-attachment.eml
│ │ │ └── tags.eml
│ │ └── utils.go
│ └── tools/
│ ├── argsparser.go
│ ├── fs.go
│ ├── headers.go
│ ├── html.go
│ ├── listunsubscribeparser.go
│ ├── net.go
│ ├── snippets.go
│ ├── tags.go
│ ├── tools_test.go
│ ├── unixsocket.go
│ └── utils.go
├── main.go
├── package.json
├── sendmail/
│ ├── cmd/
│ │ ├── cmd.go
│ │ └── smtp.go
│ └── main.go
└── server/
├── apiv1/
│ ├── api.go
│ ├── application.go
│ ├── chaos.go
│ ├── message.go
│ ├── messages.go
│ ├── other.go
│ ├── release.go
│ ├── send.go
│ ├── structs.go
│ ├── swagger-config.yml
│ ├── swaggerParams.go
│ ├── swaggerResponses.go
│ ├── tags.go
│ ├── testing.go
│ └── thumbnails.go
├── cors.go
├── cors_test.go
├── embed.go
├── handlers/
│ ├── k8healthz.go
│ ├── k8sready.go
│ ├── messages.go
│ └── proxy.go
├── server.go
├── server_test.go
├── ui/
│ └── api/
│ └── v1/
│ ├── index.html
│ └── swagger.json
├── ui-src/
│ ├── App.vue
│ ├── app.js
│ ├── assets/
│ │ ├── _bootstrap.scss
│ │ ├── _bootstrap_variables.scss
│ │ └── styles.scss
│ ├── components/
│ │ ├── AjaxLoader.vue
│ │ ├── AppAbout.vue
│ │ ├── AppBadge.vue
│ │ ├── AppFavicon.vue
│ │ ├── AppNotifications.vue
│ │ ├── AppSettings.vue
│ │ ├── EditTags.vue
│ │ ├── ListMessages.vue
│ │ ├── NavMailbox.vue
│ │ ├── NavPagination.vue
│ │ ├── NavSearch.vue
│ │ ├── NavSelected.vue
│ │ ├── NavTags.vue
│ │ ├── SearchForm.vue
│ │ └── message/
│ │ ├── HTMLCheck.vue
│ │ ├── LinkCheck.vue
│ │ ├── MessageAttachments.vue
│ │ ├── MessageHeaders.vue
│ │ ├── MessageItem.vue
│ │ ├── MessageRelease.vue
│ │ ├── MessageScreenshot.vue
│ │ └── SpamAssassin.vue
│ ├── docs.js
│ ├── mixins/
│ │ ├── CommonMixins.js
│ │ └── MessagesMixins.js
│ ├── router/
│ │ └── index.js
│ ├── stores/
│ │ ├── mailbox.js
│ │ └── pagination.js
│ └── views/
│ ├── MailboxView.vue
│ ├── MessageView.vue
│ ├── NotFoundView.vue
│ └── SearchView.vue
├── webhook/
│ └── webhook.go
└── websockets/
├── client.go
└── hub.go
SYMBOL INDEX (595 symbols across 107 files)
FILE: cmd/dump.go
function init (line 27) | func init() {
FILE: cmd/ingest.go
function init (line 132) | func init() {
function isFile (line 140) | func isFile(path string) bool {
function format (line 150) | func format(n int) string {
FILE: cmd/readyz.go
function init (line 57) | func init() {
FILE: cmd/reindex.go
function init (line 34) | func init() {
FILE: cmd/root.go
function Execute (line 63) | func Execute() {
function init (line 70) | func init() {
function initConfigFromEnv (line 195) | func initConfigFromEnv() {
function initDeprecatedConfigFromEnv (line 404) | func initDeprecatedConfigFromEnv() {
function getEnabledFromEnv (line 447) | func getEnabledFromEnv(k string) bool {
FILE: cmd/sendmail.go
function init (line 19) | func init() {
FILE: cmd/version.go
function init (line 58) | func init() {
FILE: config/config.go
type autoTag (line 236) | type autoTag struct
type SMTPRelayConfigStruct (line 242) | type SMTPRelayConfigStruct struct
type SMTPForwardConfigStruct (line 266) | type SMTPForwardConfigStruct struct
function VerifyConfig (line 283) | func VerifyConfig() error {
FILE: config/tags.go
type yamlTags (line 22) | type yamlTags struct
type yamlTag (line 26) | type yamlTag struct
function loadTagsFromConfig (line 32) | func loadTagsFromConfig(c string) error {
function loadTagsFromArgs (line 68) | func loadTagsFromArgs(c string) error {
function parseTagsDisable (line 91) | func parseTagsDisable(s string) error {
FILE: config/utils.go
function isFile (line 14) | func isFile(path string) bool {
function isDir (line 21) | func isDir(path string) bool {
function isValidURL (line 30) | func isValidURL(s string) bool {
function DBTenantID (line 40) | func DBTenantID(s string) string {
FILE: config/validators.go
function parseMaxAge (line 19) | func parseMaxAge() error {
function parseRelayConfig (line 52) | func parseRelayConfig(c string) error {
function validateRelayConfig (line 88) | func validateRelayConfig() error {
function parseForwardConfig (line 160) | func parseForwardConfig(c string) error {
function validateForwardConfig (line 188) | func validateForwardConfig() error {
function parseChaosTriggers (line 259) | func parseChaosTriggers() error {
FILE: internal/auth/auth.go
function SetUIAuth (line 23) | func SetUIAuth(s string) error {
function SetSendAPIAuth (line 42) | func SetSendAPIAuth(s string) error {
function SetSMTPAuth (line 61) | func SetSMTPAuth(s string) error {
function SetPOP3Auth (line 80) | func SetPOP3Auth(s string) error {
function credentialsFromString (line 98) | func credentialsFromString(s string) []string {
FILE: internal/dump/dump.go
function Sync (line 36) | func Sync(d string) error {
function loadIDs (line 70) | func loadIDs() error {
function saveMessages (line 118) | func saveMessages() error {
FILE: internal/html2text/html2text.go
function init (line 22) | func init() {
function Strip (line 33) | func Strip(h string, includeLinks bool) string {
function extract (line 47) | func extract(node *html.Node, buff *bytes.Buffer, includeLinks bool) {
function clean (line 68) | func clean(text string) string {
FILE: internal/html2text/html2text_test.go
function TestPlain (line 5) | func TestPlain(t *testing.T) {
function TestWithLinks (line 31) | func TestWithLinks(t *testing.T) {
function BenchmarkPlain (line 58) | func BenchmarkPlain(b *testing.B) {
function BenchmarkLinks (line 64) | func BenchmarkLinks(b *testing.B) {
FILE: internal/htmlcheck/caniemail.go
type CanIEmail (line 30) | type CanIEmail struct
type JSONResult (line 44) | type JSONResult struct
function loadJSONData (line 61) | func loadJSONData() error {
FILE: internal/htmlcheck/css.go
function runCSSTests (line 25) | func runCSSTests(html string) ([]Warning, int, error) {
function mergeInlineCSS (line 118) | func mergeInlineCSS(html string) (string, error) {
function inlineRemoteCSS (line 133) | func inlineRemoteCSS(h string) (string, error) {
function downloadCSSToBytes (line 192) | func downloadCSSToBytes(url string) ([]byte, error) {
function isValidURL (line 233) | func isValidURL(str string) bool {
function testInlineStyles (line 240) | func testInlineStyles(doc *goquery.Document) map[string]int {
function newSafeHTTPClient (line 275) | func newSafeHTTPClient() *http.Client {
FILE: internal/htmlcheck/html.go
function runHTMLTests (line 12) | func runHTMLTests(html string) ([]Warning, int, error) {
FILE: internal/htmlcheck/inline_test.go
function TestInlineStyleDetection (line 12) | func TestInlineStyleDetection(t *testing.T) {
function assertEqual (line 75) | func assertEqual(t *testing.T, a any, b any, message string) {
FILE: internal/htmlcheck/main.go
function RunTests (line 17) | func RunTests(html string) (Response, error) {
method getTest (line 107) | func (c CanIEmail) getTest(k string) (Warning, error) {
function mdToHTML (line 188) | func mdToHTML(str string) string {
FILE: internal/htmlcheck/platforms.go
function Platforms (line 10) | func Platforms() (map[string][]string, error) {
FILE: internal/htmlcheck/structs.go
type Response (line 6) | type Response struct
type Warning (line 18) | type Warning struct
type Result (line 44) | type Result struct
type Score (line 62) | type Score struct
type Total (line 76) | type Total struct
FILE: internal/linkcheck/linkcheck_test.go
function TestLinkDetection (line 66) | func TestLinkDetection(t *testing.T) {
FILE: internal/linkcheck/main.go
function RunTests (line 16) | func RunTests(msg *storage.Message, followRedirects bool) (Response, err...
function extractTextLinks (line 32) | func extractTextLinks(msg *storage.Message) []string {
function extractHTMLLinks (line 59) | func extractHTMLLinks(msg *storage.Message) []string {
function strUnique (line 98) | func strUnique(strSlice []string) []string {
FILE: internal/linkcheck/status.go
function getHTTPStatuses (line 20) | func getHTTPStatuses(links []string, followRedirects bool) []Link {
function doHead (line 69) | func doHead(link string, followRedirects bool) (int, error) {
function httpErrorSummary (line 127) | func httpErrorSummary(err error) string {
function safeDialContext (line 140) | func safeDialContext(dialer *net.Dialer) func(ctx context.Context, netwo...
FILE: internal/linkcheck/structs.go
type Response (line 6) | type Response struct
type Link (line 14) | type Link struct
FILE: internal/logger/logger.go
function Log (line 27) | func Log() *logrus.Logger {
function PrettyPrint (line 64) | func PrettyPrint(i any) {
function CleanHTTPIP (line 71) | func CleanHTTPIP(s string) string {
FILE: internal/pop3/functions.go
function authUser (line 15) | func authUser(username, password string) bool {
function sendResponse (line 20) | func sendResponse(c net.Conn, m string) {
function sendData (line 31) | func sendData(c net.Conn, m string) {
function getMessages (line 36) | func getMessages() ([]message, error) {
function getTop (line 54) | func getTop(id string, nr int) (string, string, error) {
function getCommand (line 72) | func getCommand(line string) (string, []string) {
function getSafeArg (line 78) | func getSafeArg(args []string, nr int) (string, error) {
FILE: internal/pop3/pop3_test.go
function TestPOP3 (line 25) | func TestPOP3(t *testing.T) {
function TestAuthentication (line 211) | func TestAuthentication(t *testing.T) {
function setup (line 277) | func setup() {
function connectAuth (line 308) | func connectAuth() (*pop3client.Conn, error) {
function connectBadAuth (line 320) | func connectBadAuth() (*pop3client.Conn, error) {
function connect (line 332) | func connect() (*pop3client.Conn, error) {
function portFree (line 347) | func portFree(port int) bool {
function randRange (line 360) | func randRange(min, max int) int {
function insertEmailData (line 364) | func insertEmailData(t *testing.T) {
function assertEqual (line 400) | func assertEqual(t *testing.T, a any, b any, message string) {
FILE: internal/pop3/server.go
constant AUTHORIZATION (line 27) | AUTHORIZATION = 1
constant TRANSACTION (line 29) | TRANSACTION = 2
constant UPDATE (line 31) | UPDATE = 3
function Run (line 35) | func Run() {
type message (line 81) | type message struct
function handleClient (line 86) | func handleClient(conn net.Conn) {
function handleTransactionCommand (line 211) | func handleTransactionCommand(conn net.Conn, cmd string, args []string, ...
FILE: internal/pop3client/client.go
type Client (line 19) | type Client struct
method NewConn (line 89) | func (c *Client) NewConn() (*Conn, error) {
type Conn (line 25) | type Conn struct
method Send (line 127) | func (c *Conn) Send(b string) error {
method Cmd (line 141) | func (c *Conn) Cmd(cmd string, isMulti bool, args ...any) (*bytes.Buff...
method ReadOne (line 174) | func (c *Conn) ReadOne() ([]byte, error) {
method ReadAll (line 186) | func (c *Conn) ReadAll() (*bytes.Buffer, error) {
method Auth (line 212) | func (c *Conn) Auth(user, password string) error {
method User (line 227) | func (c *Conn) User(s string) error {
method Pass (line 234) | func (c *Conn) Pass(s string) error {
method Stat (line 241) | func (c *Conn) Stat() (int, int, error) {
method List (line 271) | func (c *Conn) List(msgID int) ([]MessageID, error) {
method Uidl (line 319) | func (c *Conn) Uidl(msgID int) ([]MessageID, error) {
method Retr (line 360) | func (c *Conn) Retr(msgID int) (*mail.Message, error) {
method RetrRaw (line 376) | func (c *Conn) RetrRaw(msgID int) (*bytes.Buffer, error) {
method Top (line 382) | func (c *Conn) Top(msgID int, numLines int) (*mail.Message, error) {
method Dele (line 398) | func (c *Conn) Dele(msgID ...int) error {
method Rset (line 409) | func (c *Conn) Rset() error {
method Noop (line 416) | func (c *Conn) Noop() error {
method Quit (line 424) | func (c *Conn) Quit() error {
type Opt (line 32) | type Opt struct
type Dialer (line 48) | type Dialer interface
type MessageID (line 53) | type MessageID struct
function New (line 71) | func New(opt Opt) *Client {
function parseResp (line 437) | func parseResp(b []byte) ([]byte, error) {
FILE: internal/prometheus/metrics.go
function initMetrics (line 35) | func initMetrics() {
function updateMetrics (line 110) | func updateMetrics() {
function GetHandler (line 134) | func GetHandler() http.Handler {
function StartUpdater (line 141) | func StartUpdater() {
function StartSeparateServer (line 157) | func StartSeparateServer() {
function GetMode (line 180) | func GetMode() string {
FILE: internal/smtpd/chaos/chaos.go
type Triggers (line 30) | type Triggers struct
type Trigger (line 42) | type Trigger struct
method Trigger (line 113) | func (c Trigger) Trigger() (bool, int) {
function SetFromStruct (line 55) | func SetFromStruct(c Triggers) error {
function Set (line 82) | func Set(key string, errorCode int, probability int) error {
FILE: internal/smtpd/forward.go
function autoForwardMessage (line 17) | func autoForwardMessage(from string, data *[]byte) error {
function createForwardingSMTPClient (line 34) | func createForwardingSMTPClient(config config.SMTPForwardConfigStruct, a...
function forward (line 81) | func forward(from string, msg []byte) error {
function forwardAuthFromConfig (line 139) | func forwardAuthFromConfig() smtp.Auth {
FILE: internal/smtpd/main.go
function mailHandler (line 32) | func mailHandler(origin net.Addr, from string, to []string, data []byte,...
function SaveToDatabase (line 37) | func SaveToDatabase(origin net.Addr, from string, to []string, data []by...
function authHandler (line 156) | func authHandler(remoteAddr net.Addr, mechanism string, username []byte,...
function authHandlerAny (line 168) | func authHandlerAny(remoteAddr net.Addr, mechanism string, username []by...
function handlerRcpt (line 175) | func handlerRcpt(remoteAddr net.Addr, from string, to string) bool {
function Listen (line 191) | func Listen() error {
function verbLogTranslator (line 210) | func verbLogTranslator(verb string) string {
function listenAndServe (line 218) | func listenAndServe(addr string, handler MsgIDHandler, authHandler AuthH...
function cleanIP (line 323) | func cleanIP(i net.Addr) string {
function scanAddressesInHeader (line 331) | func scanAddressesInHeader(h mail.Header) (map[string]bool, bool) {
FILE: internal/smtpd/relay.go
function autoRelayMessage (line 17) | func autoRelayMessage(from string, to []string, data *[]byte) error {
function createRelaySMTPClient (line 69) | func createRelaySMTPClient(config config.SMTPRelayConfigStruct, addr str...
function Relay (line 116) | func Relay(from string, to []string, msg []byte) error {
function relayAuthFromConfig (line 172) | func relayAuthFromConfig() smtp.Auth {
type loginAuth (line 192) | type loginAuth struct
method Start (line 204) | func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
method Next (line 208) | func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
function LoginAuth (line 197) | func LoginAuth(username, password string) smtp.Auth {
FILE: internal/smtpd/smtpd.go
type Handler (line 42) | type Handler
type MsgIDHandler (line 46) | type MsgIDHandler
type HandlerRcpt (line 49) | type HandlerRcpt
type AuthHandler (line 52) | type AuthHandler
function ListenAndServe (line 60) | func ListenAndServe(addr string, handler Handler, appName string, hostna...
function ListenAndServeTLS (line 68) | func ListenAndServeTLS(addr string, certFile string, keyFile string, han...
type maxSizeExceededError (line 77) | type maxSizeExceededError struct
method Error (line 87) | func (err maxSizeExceededError) Error() string {
function maxSizeExceeded (line 81) | func maxSizeExceeded(limit int) maxSizeExceededError {
type LogFunc (line 92) | type LogFunc
type Server (line 95) | type Server struct
method ConfigureTLS (line 127) | func (srv *Server) ConfigureTLS(certFile string, keyFile string) error {
method ListenAndServe (line 171) | func (srv *Server) ListenAndServe() error {
method Serve (line 217) | func (srv *Server) Serve(ln net.Listener) error {
method newSession (line 264) | func (srv *Server) newSession(conn net.Conn) (s *session) {
method getShutdownChan (line 299) | func (srv *Server) getShutdownChan() <-chan struct{} {
method closeShutdownChan (line 309) | func (srv *Server) closeShutdownChan() {
method Close (line 324) | func (srv *Server) Close() error {
method Shutdown (line 331) | func (srv *Server) Shutdown(ctx context.Context) error {
type session (line 246) | type session struct
method serve (line 359) | func (s *session) serve() {
method writef (line 789) | func (s *session) writef(format string, args ...any) {
method readLine (line 809) | func (s *session) readLine() (string, error) {
method parseLine (line 833) | func (s *session) parseLine(line string) (verb string, args string) {
method readData (line 845) | func (s *session) readData() ([]byte, error) {
method makeHeaders (line 880) | func (s *session) makeHeaders(to []string) []byte {
method authMechs (line 892) | func (s *session) authMechs() (mechs map[string]bool) {
method makeEHLOResponse (line 906) | func (s *session) makeEHLOResponse() (response string) {
method handleAuthLogin (line 939) | func (s *session) handleAuthLogin(arg string) (bool, error) {
method handleAuthPlain (line 978) | func (s *session) handleAuthPlain(arg string) (bool, error) {
method handleAuthCramMD5 (line 1012) | func (s *session) handleAuthCramMD5() (bool, error) {
function extractAndValidateAddress (line 1044) | func extractAndValidateAddress(re *regexp.Regexp, args string) ([]string...
FILE: internal/smtpd/smtpd_test.go
function newConn (line 25) | func newConn(t *testing.T, server *Server) net.Conn {
function cmdCode (line 41) | func cmdCode(t *testing.T, conn net.Conn, cmd string, code string) string {
function TestSimpleCommands (line 56) | func TestSimpleCommands(t *testing.T) {
function TestCmdHELO (line 80) | func TestCmdHELO(t *testing.T) {
function TestCmdEHLO (line 97) | func TestCmdEHLO(t *testing.T) {
function TestCmdMAILBeforeEHLO (line 127) | func TestCmdMAILBeforeEHLO(t *testing.T) {
function TestCmdMAILAfterRCPT (line 137) | func TestCmdMAILAfterRCPT(t *testing.T) {
function TestCmdRSET (line 162) | func TestCmdRSET(t *testing.T) {
function TestCmdMAIL (line 176) | func TestCmdMAIL(t *testing.T) {
function TestCmdMAILMaxSize (line 231) | func TestCmdMAILMaxSize(t *testing.T) {
function TestCmdRCPT (line 261) | func TestCmdRCPT(t *testing.T) {
function TestCmdMaxRecipients (line 309) | func TestCmdMaxRecipients(t *testing.T) {
function TestCmdDATA (line 326) | func TestCmdDATA(t *testing.T) {
function TestCmdDATAWithMaxSize (line 356) | func TestCmdDATAWithMaxSize(t *testing.T) {
type mockHandler (line 393) | type mockHandler struct
method handler (line 397) | func (m *mockHandler) handler(err error) func(a net.Addr, f string, t ...
function TestCmdDATAWithHandler (line 404) | func TestCmdDATAWithHandler(t *testing.T) {
function TestCmdDATAWithHandlerError (line 421) | func TestCmdDATAWithHandlerError(t *testing.T) {
function TestCmdSTARTTLS (line 438) | func TestCmdSTARTTLS(t *testing.T) {
function TestCmdSTARTTLSFailure (line 452) | func TestCmdSTARTTLSFailure(t *testing.T) {
function makeCertificate (line 482) | func makeCertificate() tls.Certificate {
function TestCmdSTARTTLSSuccess (line 541) | func TestCmdSTARTTLSSuccess(t *testing.T) {
function TestCmdSTARTTLSRequired (line 567) | func TestCmdSTARTTLSRequired(t *testing.T) {
function TestMakeHeaders (line 618) | func TestMakeHeaders(t *testing.T) {
function TestParseLine (line 634) | func TestParseLine(t *testing.T) {
function TestReadLine (line 655) | func TestReadLine(t *testing.T) {
function TestReadData (line 680) | func TestReadData(t *testing.T) {
function TestReadDataWithMaxSize (line 723) | func TestReadDataWithMaxSize(t *testing.T) {
function parseExtensions (line 756) | func parseExtensions(t *testing.T, greeting string) map[string]string {
function testAuthHandler (line 795) | func testAuthHandler(_ net.Addr, _ string, username []byte, _ []byte, _ ...
function TestMakeEHLOResponse (line 800) | func TestMakeEHLOResponse(t *testing.T) {
function TestCmd8BITMIME (line 908) | func TestCmd8BITMIME(t *testing.T) {
function TestAuthMechs (line 1069) | func TestAuthMechs(t *testing.T) {
function TestCmdAUTH (line 1123) | func TestCmdAUTH(t *testing.T) {
function TestCmdAUTHOptional (line 1135) | func TestCmdAUTHOptional(t *testing.T) {
function TestCmdAUTHRequired (line 1176) | func TestCmdAUTHRequired(t *testing.T) {
function TestCmdAUTHLOGIN (line 1225) | func TestCmdAUTHLOGIN(t *testing.T) {
function TestCmdAUTHLOGINFast (line 1282) | func TestCmdAUTHLOGINFast(t *testing.T) {
function TestCmdAUTHPLAIN (line 1334) | func TestCmdAUTHPLAIN(t *testing.T) {
function TestCmdAUTHPLAINEmpty (line 1393) | func TestCmdAUTHPLAINEmpty(t *testing.T) {
function TestCmdAUTHPLAINFast (line 1452) | func TestCmdAUTHPLAINFast(t *testing.T) {
function TestCmdAUTHPLAINFastAndEmpty (line 1504) | func TestCmdAUTHPLAINFastAndEmpty(t *testing.T) {
function makeCRAMMD5Response (line 1557) | func makeCRAMMD5Response(challenge string, username string, secret strin...
function TestCmdAUTHCRAMMD5 (line 1569) | func TestCmdAUTHCRAMMD5(t *testing.T) {
function TestCmdAUTHCRAMMD5WithTLS (line 1626) | func TestCmdAUTHCRAMMD5WithTLS(t *testing.T) {
function BenchmarkReceive (line 1693) | func BenchmarkReceive(b *testing.B) {
function TestCmdShutdown (line 1721) | func TestCmdShutdown(t *testing.T) {
type mockDropRejectedHandler (line 1764) | type mockDropRejectedHandler struct
method handler (line 1773) | func (m *mockDropRejectedHandler) handler(remoteAddr net.Addr, from st...
method msgIDHandler (line 1780) | func (m *mockDropRejectedHandler) msgIDHandler(remoteAddr net.Addr, fr...
function TestIgnoreRejectedRecipients (line 1788) | func TestIgnoreRejectedRecipients(t *testing.T) {
FILE: internal/snakeoil/snakeoil.go
type KeyPair (line 27) | type KeyPair struct
function Certificates (line 34) | func Certificates() map[string]KeyPair {
function Public (line 39) | func Public(str string) string {
function Private (line 65) | func Private(str string) string {
function parse (line 93) | func parse(str string) ([]string, string, error) {
function generate (line 118) | func generate(domains []string) (string, string, error) {
FILE: internal/spamassassin/postmark/postmark.go
type Response (line 16) | type Response struct
type Rule (line 25) | type Rule struct
function Check (line 33) | func Check(email []byte, timeout int) (Response, error) {
function nameFromReport (line 88) | func nameFromReport(score, description, report string) string {
FILE: internal/spamassassin/spamassassin.go
type Result (line 29) | type Result struct
type Rule (line 41) | type Rule struct
function SetService (line 51) | func SetService(s string) {
function Ping (line 61) | func Ping() error {
function Check (line 77) | func Check(msg []byte) (Result, error) {
function round1dm (line 138) | func round1dm(n float64) float64 {
FILE: internal/spamassassin/spamc/spamc.go
constant ProtoVersion (line 21) | ProtoVersion = "1.5"
type connection (line 32) | type connection interface
type Client (line 38) | type Client struct
method dial (line 72) | func (c *Client) dial() (connection, error) {
method Report (line 92) | func (c *Client) Report(email []byte) (Result, error) {
method report (line 101) | func (c *Client) report(email []byte) ([]string, error) {
method parseOutput (line 162) | func (c *Client) parseOutput(output []string) Result {
method Ping (line 220) | func (c *Client) Ping() error {
function NewTCP (line 45) | func NewTCP(addr string, timeout int) *Client {
function NewUnix (line 50) | func NewUnix(addr string) *Client {
type Rule (line 55) | type Rule struct
type Result (line 62) | type Result struct
FILE: internal/stats/stats.go
type versionCache (line 17) | type versionCache struct
type AppInformation (line 44) | type AppInformation struct
function getBackoff (line 79) | func getBackoff(errCount int) time.Duration {
function Load (line 85) | func Load(detectLatestVersion bool) AppInformation {
function Track (line 142) | func Track() {
function LogSMTPAccepted (line 147) | func LogSMTPAccepted(size int) {
function LogSMTPRejected (line 155) | func LogSMTPRejected() {
function LogSMTPIgnored (line 162) | func LogSMTPIgnored() {
FILE: internal/storage/cron.go
function dbCron (line 18) | func dbCron() {
function pruneMessages (line 51) | func pruneMessages() {
function vacuumDb (line 181) | func vacuumDb() {
FILE: internal/storage/database.go
function InitDB (line 41) | func InitDB() error {
function tenant (line 166) | func tenant(table string) string {
function Close (line 171) | func Close() {
function Ping (line 189) | func Ping() error {
function StatsGet (line 194) | func StatsGet() MailboxStats {
function CountTotal (line 211) | func CountTotal() uint64 {
function CountUnread (line 222) | func CountUnread() uint64 {
function CountRead (line 234) | func CountRead() uint64 {
function DbSize (line 246) | func DbSize() uint64 {
function MessageIDExists (line 259) | func MessageIDExists(id string) bool {
FILE: internal/storage/functions_test.go
function setup (line 19) | func setup(tenantID string) {
function assertEqual (line 52) | func assertEqual(t *testing.T, a any, b any, message string) {
function assertEqualStats (line 60) | func assertEqualStats(t *testing.T, total int, unread int) {
FILE: internal/storage/messages.go
function Store (line 33) | func Store(body *[]byte, username *string) (string, error) {
function List (line 215) | func List(start int, beforeTS int64, limit int) ([]MessageSummary, error) {
function GetMessage (line 296) | func GetMessage(id string) (*Message, error) {
function GetMessageRaw (line 419) | func GetMessageRaw(id string) ([]byte, error) {
function GetAttachmentPart (line 461) | func GetAttachmentPart(id, partID string) (*enmime.Part, error) {
function AttachmentSummary (line 500) | func AttachmentSummary(a *enmime.Part) Attachment {
function LatestID (line 526) | func LatestID(r *http.Request) (string, error) {
function MarkRead (line 550) | func MarkRead(ids []string) error {
function MarkAllRead (line 575) | func MarkAllRead() error {
function MarkAllUnread (line 600) | func MarkAllUnread() error {
function MarkUnread (line 625) | func MarkUnread(ids []string) error {
function DeleteMessages (line 652) | func DeleteMessages(ids []string) error {
function DeleteAllMessages (line 750) | func DeleteAllMessages() error {
function GetMetadata (line 804) | func GetMetadata(id string) (Metadata, error) {
FILE: internal/storage/messages_test.go
function TestTextEmailInserts (line 11) | func TestTextEmailInserts(t *testing.T) {
function TestMimeEmailInserts (line 43) | func TestMimeEmailInserts(t *testing.T) {
function TestRetrieveMimeEmail (line 82) | func TestRetrieveMimeEmail(t *testing.T) {
function TestMessageSummary (line 143) | func TestMessageSummary(t *testing.T) {
function BenchmarkImportText (line 184) | func BenchmarkImportText(b *testing.B) {
function BenchmarkImportMime (line 196) | func BenchmarkImportMime(b *testing.B) {
function TestInlineImageContentIdHandling (line 209) | func TestInlineImageContentIdHandling(t *testing.T) {
function TestRegularAttachmentHandling (line 239) | func TestRegularAttachmentHandling(t *testing.T) {
function TestMixedAttachmentHandling (line 273) | func TestMixedAttachmentHandling(t *testing.T) {
FILE: internal/storage/notifications.go
function BroadcastMailboxStats (line 16) | func BroadcastMailboxStats() {
FILE: internal/storage/reindex.go
function ReindexAll (line 20) | func ReindexAll() {
function chunkBy (line 139) | func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
FILE: internal/storage/schemas.go
function dbApplySchemas (line 20) | func dbApplySchemas() error {
function dataMigrations (line 147) | func dataMigrations() {
FILE: internal/storage/schemas/1.0.0.sql
type tenant (line 2) | CREATE TABLE IF NOT EXISTS {{ tenant "mailbox" }} (
type "idx_id" (line 11) | CREATE UNIQUE INDEX IF NOT EXISTS {{ tenant "idx_id" }} ON {{ tenant "ma...
type "idx_read" (line 12) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_read" }} ON {{ tenant "mailbox...
FILE: internal/storage/schemas/1.1.0.sql
type "idx_tags" (line 3) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_tags" }} ON {{ tenant "mailbox...
FILE: internal/storage/schemas/1.2.0.sql
type tenant (line 2) | CREATE TABLE IF NOT EXISTS {{ tenant "mailboxtmp" }} (
type "idx_id" (line 29) | CREATE UNIQUE INDEX IF NOT EXISTS {{ tenant "idx_id" }} ON {{ tenant "ma...
type "idx_message_id" (line 30) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_message_id" }} ON {{ tenant "m...
type "idx_subject" (line 31) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_subject" }} ON {{ tenant "mail...
type "idx_size" (line 32) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_size" }} ON {{ tenant "mailbox...
type "idx_inline" (line 33) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_inline" }} ON {{ tenant "mailb...
type "idx_attachments" (line 34) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_attachments" }} ON {{ tenant "...
type "idx_read" (line 35) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_read" }} ON {{ tenant "mailbox...
type "idx_tags" (line 36) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_tags" }} ON {{ tenant "mailbox...
FILE: internal/storage/schemas/1.21.8.sql
type tenant (line 9) | CREATE TABLE IF NOT EXISTS {{ tenant "message_tags" }} (
type "idx_message_tags_tagid" (line 16) | CREATE INDEX IF NOT EXISTS {{ tenant "idx_message_tags_tagid" }} ON {{ t...
FILE: internal/storage/schemas/1.4.0.sql
type tenant (line 2) | CREATE TABLE IF NOT EXISTS {{ tenant "tags" }} (
FILE: internal/storage/schemas/1.5.0.sql
type tenant (line 2) | CREATE TABLE IF NOT EXISTS {{ tenant "settings" }} (
FILE: internal/storage/search.go
function Search (line 24) | func Search(search, timezone string, start int, beforeTS int64, limit in...
function SearchUnreadCount (line 102) | func SearchUnreadCount(search, timezone string, beforeTS int64) (int64, ...
function DeleteSearch (line 137) | func DeleteSearch(search, timezone string) error {
function SetSearchReadStatus (line 258) | func SetSearchReadStatus(search, timezone string, read bool) error {
function searchQueryBuilder (line 299) | func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt {
function sizeToBytes (line 519) | func sizeToBytes(v string) uint64 {
FILE: internal/storage/search_test.go
function TestSearch (line 13) | func TestSearch(t *testing.T) {
function TestSearchDelete100 (line 107) | func TestSearchDelete100(t *testing.T) {
function TestSearchDelete1100 (line 155) | func TestSearchDelete1100(t *testing.T) {
function TestEscPercentChar (line 189) | func TestEscPercentChar(t *testing.T) {
function TestSizeToBytes (line 205) | func TestSizeToBytes(t *testing.T) {
FILE: internal/storage/settings.go
function SettingGet (line 12) | func SettingGet(k string) string {
function SettingPut (line 28) | func SettingPut(k, v string) error {
function getDeletedSize (line 38) | func getDeletedSize() uint64 {
function totalMessagesSize (line 54) | func totalMessagesSize() uint64 {
function addDeletedSize (line 68) | func addDeletedSize(v uint64) {
FILE: internal/storage/structs.go
type Message (line 11) | type Message struct
type Attachment (line 54) | type Attachment struct
type MessageSummary (line 79) | type MessageSummary struct
type MailboxStats (line 113) | type MailboxStats struct
type Metadata (line 120) | type Metadata struct
type ListUnsubscribe (line 131) | type ListUnsubscribe struct
FILE: internal/storage/tagfilters.go
type TagFilter (line 15) | type TagFilter struct
function LoadTagFilters (line 27) | func LoadTagFilters() {
function tagFilterMatches (line 60) | func tagFilterMatches(id string) []string {
FILE: internal/storage/tags.go
function SetMessageTags (line 26) | func SetMessageTags(id string, tags []string) ([]string, error) {
function addMessageTag (line 75) | func addMessageTag(id, name string) (string, error) {
function deleteMessageTag (line 130) | func deleteMessageTag(id, name string) error {
function GetAllTags (line 142) | func GetAllTags() []string {
function GetAllTagsCount (line 160) | func GetAllTagsCount() map[string]int64 {
function RenameTag (line 182) | func RenameTag(from, to string) error {
function DeleteTag (line 223) | func DeleteTag(tag string) error {
function pruneUnusedTags (line 255) | func pruneUnusedTags() error {
function findTagsInRawMessage (line 298) | func findTagsInRawMessage(message *[]byte) []string {
method tagsFromPlusAddresses (line 315) | func (d Metadata) tagsFromPlusAddresses() []string {
function getMessageTags (line 345) | func getMessageTags(id string) []string {
function sortedUniqueTags (line 366) | func sortedUniqueTags(s []string) []string {
FILE: internal/storage/tags_test.go
function TestTags (line 14) | func TestTags(t *testing.T) {
function TestUsernameAutoTagging (line 147) | func TestUsernameAutoTagging(t *testing.T) {
function deleteAllMessageTags (line 188) | func deleteAllMessageTags(id string) error {
FILE: internal/storage/utils.go
function AddTempFile (line 24) | func AddTempFile(s string) {
function deleteTempFiles (line 29) | func deleteTempFiles() {
function addressToSlice (line 38) | func addressToSlice(env *enmime.Envelope, key string) []*mail.Address {
function createSearchText (line 49) | func createSearchText(env *enmime.Envelope) string {
function cleanString (line 77) | func cleanString(str string) string {
function logMessagesDeleted (line 90) | func logMessagesDeleted(n int) {
function isFile (line 97) | func isFile(path string) bool {
function escPercentChar (line 107) | func escPercentChar(s string) string {
FILE: internal/tools/argsparser.go
function ArgsParser (line 6) | func ArgsParser(s string) []string {
FILE: internal/tools/fs.go
function IsFile (line 9) | func IsFile(path string) bool {
function IsDir (line 16) | func IsDir(path string) bool {
FILE: internal/tools/headers.go
function RemoveMessageHeaders (line 16) | func RemoveMessageHeaders(msg []byte, headers []string) ([]byte, error) {
function SetMessageHeader (line 64) | func SetMessageHeader(msg []byte, header, value string) ([]byte, error) {
function OverrideFromHeader (line 103) | func OverrideFromHeader(msg []byte, address string) ([]byte, error) {
FILE: internal/tools/html.go
function GetHTMLAttributeVal (line 11) | func GetHTMLAttributeVal(e *html.Node, key string) (string, error) {
function SetHTMLAttributeVal (line 22) | func SetHTMLAttributeVal(n *html.Node, key, val string) {
function WalkHTML (line 37) | func WalkHTML(n *html.Node, fn func(*html.Node)) {
FILE: internal/tools/listunsubscribeparser.go
function ListUnsubscribeParser (line 12) | func ListUnsubscribeParser(v string) ([]string, error) {
FILE: internal/tools/net.go
function IsInternalIP (line 15) | func IsInternalIP(ip net.IP) bool {
function IsValidLinkURL (line 25) | func IsValidLinkURL(str string) bool {
FILE: internal/tools/snippets.go
function CreateSnippet (line 12) | func CreateSnippet(text, html string) string {
function truncate (line 49) | func truncate(s string, n int) string {
FILE: internal/tools/tags.go
function CleanTag (line 24) | func CleanTag(s string) string {
function SetTagCasing (line 40) | func SetTagCasing(s []string) []string {
FILE: internal/tools/tools_test.go
function TestArgsParser (line 8) | func TestArgsParser(t *testing.T) {
function TestCleanTag (line 29) | func TestCleanTag(t *testing.T) {
function TestSnippets (line 48) | func TestSnippets(t *testing.T) {
function TestListUnsubscribeParser (line 74) | func TestListUnsubscribeParser(t *testing.T) {
FILE: internal/tools/unixsocket.go
function UnixSocket (line 15) | func UnixSocket(address string) (string, fs.FileMode, bool) {
function PrepareSocket (line 36) | func PrepareSocket(address string) error {
FILE: internal/tools/utils.go
function Plural (line 10) | func Plural(total int, singular, plural string) string {
function InArray (line 19) | func InArray(k string, arr []string) bool {
function Normalize (line 30) | func Normalize(s string) string {
function SafeUint64 (line 41) | func SafeUint64(i any) uint64 {
FILE: main.go
function main (line 13) | func main() {
FILE: sendmail/cmd/cmd.go
function init (line 44) | func init() {
function Run (line 66) | func Run() {
function HelpTemplate (line 189) | func HelpTemplate(args []string) string {
function socketAddress (line 212) | func socketAddress(address string) (string, bool) {
FILE: sendmail/cmd/smtp.go
function Send (line 20) | func Send(addr string, from string, to []string, msg []byte) error {
function sendMail (line 76) | func sendMail(addr string, a smtp.Auth, from string, to []string, msg []...
function validateLine (line 148) | func validateLine(line string) error {
FILE: sendmail/main.go
function main (line 5) | func main() {
FILE: server/apiv1/api.go
function fourOFour (line 16) | func fourOFour(w http.ResponseWriter) {
function httpError (line 25) | func httpError(w http.ResponseWriter, msg string) {
function httpJSONError (line 34) | func httpJSONError(w http.ResponseWriter, msg string) {
function getStartLimit (line 47) | func getStartLimit(req *http.Request) (start int, beforeTS int64, limit ...
function GetOptions (line 76) | func GetOptions(w http.ResponseWriter, _ *http.Request) {
FILE: server/apiv1/application.go
function AppInfo (line 14) | func AppInfo(w http.ResponseWriter, _ *http.Request) {
function WebUIConfig (line 37) | func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
FILE: server/apiv1/chaos.go
function GetChaos (line 11) | func GetChaos(w http.ResponseWriter, _ *http.Request) {
function SetChaos (line 42) | func SetChaos(w http.ResponseWriter, r *http.Request) {
FILE: server/apiv1/message.go
function GetMessage (line 16) | func GetMessage(w http.ResponseWriter, r *http.Request) {
function GetHeaders (line 62) | func GetHeaders(w http.ResponseWriter, r *http.Request) {
function DownloadAttachment (line 115) | func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
function DownloadRaw (line 167) | func DownloadRaw(w http.ResponseWriter, r *http.Request) {
FILE: server/apiv1/messages.go
type MessagesSummary (line 13) | type MessagesSummary struct
function GetMessages (line 43) | func GetMessages(w http.ResponseWriter, r *http.Request) {
function SetReadStatus (line 87) | func SetReadStatus(w http.ResponseWriter, r *http.Request) {
function DeleteMessages (line 168) | func DeleteMessages(w http.ResponseWriter, r *http.Request) {
function Search (line 209) | func Search(w http.ResponseWriter, r *http.Request) {
function DeleteSearch (line 266) | func DeleteSearch(w http.ResponseWriter, r *http.Request) {
FILE: server/apiv1/other.go
function HTMLCheck (line 19) | func HTMLCheck(w http.ResponseWriter, r *http.Request) {
function LinkCheck (line 84) | func LinkCheck(w http.ResponseWriter, r *http.Request) {
function SpamAssassinCheck (line 142) | func SpamAssassinCheck(w http.ResponseWriter, r *http.Request) {
FILE: server/apiv1/release.go
function ReleaseMessage (line 21) | func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
FILE: server/apiv1/send.go
function SendMessageHandler (line 21) | func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
method Send (line 74) | func (d sendMessageParams) Send(remoteAddr string, httpAuthUser *string)...
FILE: server/apiv1/swaggerParams.go
type setChaosParams (line 12) | type setChaosParams struct
type attachmentParams (line 18) | type attachmentParams struct
type downloadRawParams (line 33) | type downloadRawParams struct
type getMessageParams (line 42) | type getMessageParams struct
type getHeadersParams (line 51) | type getHeadersParams struct
type getMessagesParams (line 60) | type getMessagesParams struct
type setReadStatusParams (line 81) | type setReadStatusParams struct
type deleteMessagesParams (line 114) | type deleteMessagesParams struct
type searchParams (line 127) | type searchParams struct
type deleteSearchParams (line 160) | type deleteSearchParams struct
type htmlCheckParams (line 177) | type htmlCheckParams struct
type linkCheckParams (line 187) | type linkCheckParams struct
type releaseMessageParams (line 203) | type releaseMessageParams struct
type sendMessageParams (line 222) | type sendMessageParams struct
type setTagsParams (line 320) | type setTagsParams struct
type renameTagParams (line 338) | type renameTagParams struct
type deleteTagParams (line 357) | type deleteTagParams struct
type getMessageHTMLParams (line 366) | type getMessageHTMLParams struct
type getMessageTextParams (line 386) | type getMessageTextParams struct
type spamAssassinCheckParams (line 395) | type spamAssassinCheckParams struct
type thumbnailParams (line 404) | type thumbnailParams struct
FILE: server/apiv1/swaggerResponses.go
type binaryResponse (line 16) | type binaryResponse
type textResponse (line 20) | type textResponse
type htmlResponse (line 24) | type htmlResponse
type errorResponse (line 29) | type errorResponse
type notFoundResponse (line 33) | type notFoundResponse
type okResponse (line 37) | type okResponse
type arrayResponse (line 41) | type arrayResponse
type jsonErrorResponse (line 45) | type jsonErrorResponse struct
type webUIConfigurationResponse (line 58) | type webUIConfigurationResponse struct
type appInfoResponse (line 103) | type appInfoResponse struct
type chaosResponse (line 112) | type chaosResponse struct
type messageHeadersResponse (line 121) | type messageHeadersResponse
type messagesSummaryResponse (line 125) | type messagesSummaryResponse struct
type sendMessageResponse (line 133) | type sendMessageResponse struct
FILE: server/apiv1/tags.go
function GetAllTags (line 13) | func GetAllTags(w http.ResponseWriter, _ *http.Request) {
function SetMessageTags (line 36) | func SetMessageTags(w http.ResponseWriter, r *http.Request) {
function RenameTag (line 84) | func RenameTag(w http.ResponseWriter, r *http.Request) {
function DeleteTag (line 128) | func DeleteTag(w http.ResponseWriter, r *http.Request) {
FILE: server/apiv1/testing.go
function GetMessageHTML (line 20) | func GetMessageHTML(w http.ResponseWriter, r *http.Request) {
function GetMessageText (line 107) | func GetMessageText(w http.ResponseWriter, r *http.Request) {
function linkInlineImages (line 152) | func linkInlineImages(msg *storage.Message) string {
FILE: server/apiv1/thumbnails.go
function Thumbnail (line 26) | func Thumbnail(w http.ResponseWriter, r *http.Request) {
function blankImage (line 105) | func blankImage(a *enmime.Part, w http.ResponseWriter) {
FILE: server/cors.go
function asciiFoldString (line 22) | func asciiFoldString(s string) string {
function toLowerASCIIFold (line 32) | func toLowerASCIIFold(c byte) byte {
function corsOriginAccessControl (line 40) | func corsOriginAccessControl(r *http.Request) bool {
function setCORSOrigins (line 74) | func setCORSOrigins() {
function extractOrigins (line 98) | func extractOrigins(str string) []string {
FILE: server/cors_test.go
function TestExtractOrigins (line 8) | func TestExtractOrigins(t *testing.T) {
function TestCorsOriginAccessControl (line 69) | func TestCorsOriginAccessControl(t *testing.T) {
FILE: server/embed.go
function embedController (line 22) | func embedController(w http.ResponseWriter, r *http.Request) {
function contentType (line 54) | func contentType(p string) string {
FILE: server/handlers/k8healthz.go
function HealthzHandler (line 6) | func HealthzHandler(w http.ResponseWriter, _ *http.Request) {
FILE: server/handlers/k8sready.go
function ReadyzHandler (line 11) | func ReadyzHandler(isReady *atomic.Value) http.HandlerFunc {
FILE: server/handlers/messages.go
function RedirectToLatestMessage (line 13) | func RedirectToLatestMessage(w http.ResponseWriter, r *http.Request) {
FILE: server/handlers/proxy.go
constant maxProxyBodySize (line 29) | maxProxyBodySize = 50 * 1024 * 1024
type MessageAssets (line 43) | type MessageAssets struct
function init (line 51) | func init() {
function ProxyHandler (line 71) | func ProxyHandler(w http.ResponseWriter, r *http.Request) {
function getAssets (line 244) | func getAssets(id string) ([]string, error) {
function absoluteURL (line 318) | func absoluteURL(link, baseURL string) (string, error) {
function httpError (line 352) | func httpError(w http.ResponseWriter, msg string) {
function supportedProxyContentType (line 362) | func supportedProxyContentType(ct string) bool {
function safeDialContext (line 393) | func safeDialContext(dialer *net.Dialer) func(ctx context.Context, netwo...
FILE: server/server.go
type contextKey (line 43) | type contextKey
constant skipUIAuthKey (line 45) | skipUIAuthKey contextKey = iota
function Listen (line 48) | func Listen() {
function apiRoutes (line 168) | func apiRoutes() *mux.Router {
function basicAuthResponse (line 218) | func basicAuthResponse(w http.ResponseWriter) {
function sendAPIAuthMiddleware (line 228) | func sendAPIAuthMiddleware(fn http.HandlerFunc) http.HandlerFunc {
type gzipResponseWriter (line 262) | type gzipResponseWriter struct
method Write (line 267) | func (w gzipResponseWriter) Write(b []byte) (int, error) {
function middleWareFunc (line 273) | func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
function addSlashToWebroot (line 344) | func addSlashToWebroot(w http.ResponseWriter, r *http.Request) {
function apiWebsocket (line 350) | func apiWebsocket(w http.ResponseWriter, r *http.Request) {
function swaggerBasePath (line 356) | func swaggerBasePath(w http.ResponseWriter, _ *http.Request) {
function index (line 374) | func index(w http.ResponseWriter, r *http.Request) {
FILE: server/server_test.go
function TestAPIv1Messages (line 43) | func TestAPIv1Messages(t *testing.T) {
function TestAPIv1ToggleReadStatus (line 104) | func TestAPIv1ToggleReadStatus(t *testing.T) {
function TestAPIv1Search (line 188) | func TestAPIv1Search(t *testing.T) {
function TestAPIv1Send (line 219) | func TestAPIv1Send(t *testing.T) {
function TestSendAPIAuthMiddleware (line 331) | func TestSendAPIAuthMiddleware(t *testing.T) {
function setup (line 495) | func setup() {
function assertStatsEqual (line 509) | func assertStatsEqual(t *testing.T, uri string, unread, total int) {
function assertSearchEqual (line 527) | func assertSearchEqual(t *testing.T, uri, query string, count int) {
function insertEmailData (line 547) | func insertEmailData(t *testing.T) {
function fetchMessage (line 583) | func fetchMessage(url string) (storage.Message, error) {
function fetchMessages (line 598) | func fetchMessages(url string) (apiv1.MessagesSummary, error) {
function clientGet (line 613) | func clientGet(url string) ([]byte, error) {
function clientDelete (line 629) | func clientDelete(url, body string) ([]byte, error) {
function clientPut (line 654) | func clientPut(url, body string) ([]byte, error) {
function clientPost (line 679) | func clientPost(url, body string) ([]byte, error) {
function clientPostWithAuth (line 704) | func clientPostWithAuth(url, body, username, password string) ([]byte, e...
function clientGetWithAuth (line 731) | func clientGetWithAuth(url, username, password string) ([]byte, error) {
function assertEqual (line 757) | func assertEqual(t *testing.T, a any, b any, message string) {
FILE: server/ui-src/mixins/CommonMixins.js
class BootstrapElement (line 9) | class BootstrapElement {
method hide (line 10) | hide() {}
method show (line 11) | show() {}
method data (line 19) | data() {
method copyToClipboardSupported (line 28) | copyToClipboardSupported() {
method resolve (line 34) | resolve(u) {
method searchURI (line 38) | searchURI(s) {
method getFileSize (line 42) | getFileSize(bytes) {
method formatNumber (line 50) | formatNumber(nr) {
method messageDate (line 54) | messageDate(d) {
method secondsToRelative (line 58) | secondsToRelative(d) {
method tagEncodeURI (line 62) | tagEncodeURI(tag) {
method getSearch (line 70) | getSearch() {
method getPaginationParams (line 84) | getPaginationParams() {
method modal (line 99) | modal(id) {
method hideNav (line 109) | hideNav() {
method get (line 124) | get(url, values, callback, errorCallback, hideLoader) {
method post (line 153) | post(url, data, callback) {
method delete (line 174) | delete(url, data, callback) {
method put (line 195) | put(url, data, callback) {
method handleError (line 210) | handleError(error) {
method allAttachments (line 229) | allAttachments(message) {
method isImage (line 247) | isImage(a) {
method attachmentIcon (line 251) | attachmentIcon(a) {
method colorHash (line 293) | colorHash(s) {
method copyToClipboard (line 303) | copyToClipboard(text) {
FILE: server/ui-src/mixins/MessagesMixins.js
method data (line 8) | data() {
method "mailbox.refresh" (line 17) | "mailbox.refresh"(v) {
method reloadMailbox (line 27) | reloadMailbox() {
method loadMessages (line 32) | loadMessages() {
FILE: server/webhook/webhook.go
function Send (line 32) | func Send(msg any) {
FILE: server/websockets/client.go
constant writeWait (line 18) | writeWait = 10 * time.Second
constant pongWait (line 21) | pongWait = 60 * time.Second
constant pingPeriod (line 24) | pingPeriod = (pongWait * 9) / 10
type Client (line 45) | type Client struct
method readPump (line 56) | func (c *Client) readPump() {
method writePump (line 77) | func (c *Client) writePump() {
function ServeWs (line 117) | func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
function basicAuthResponse (line 147) | func basicAuthResponse(w http.ResponseWriter) {
FILE: server/websockets/hub.go
type Hub (line 13) | type Hub struct
method Run (line 44) | func (h *Hub) Run() {
type WebsocketNotification (line 28) | type WebsocketNotification struct
function NewHub (line 34) | func NewHub() *Hub {
function Broadcast (line 72) | func Broadcast(t string, msg any) {
function BroadCastClientError (line 95) | func BroadCastClientError(severity, errorType, ip, message string) {
Condensed preview — 185 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,937K chars).
[
{
"path": ".dockerignore",
"chars": 23,
"preview": "/node_modules\n/mailpit\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 90,
"preview": "# These are supported funding model platforms\n\ngithub: [axllent]\nthanks_dev: u/gh/axllent\n"
},
{
"path": ".github/SECURITY.md",
"chars": 885,
"preview": "# Reporting security vulnerabilities\n\nYour efforts to responsibly disclose your findings are appreciated.\n\n**Please do n"
},
{
"path": ".github/cliff.toml",
"chars": 1852,
"preview": "## https://git-cliff.org/\n[changelog]\nbody = \"\"\"\n{% if version %}\\\n \\n## [{{ version }}]\n{% else %}\\\n \\n## Unreleased\n"
},
{
"path": ".github/dependabot.yml",
"chars": 815,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/build-docker-edge.yml",
"chars": 1385,
"preview": "on:\n push:\n branches: [ develop ]\n\nname: Build docker edge images\njobs:\n docker:\n runs-on: ubuntu-latest\n ste"
},
{
"path": ".github/workflows/build-docker.yml",
"chars": 1604,
"preview": "on:\n release:\n types: [created]\n\nname: Build docker images\njobs:\n docker:\n runs-on: ubuntu-latest\n steps:\n "
},
{
"path": ".github/workflows/close-stale-issues.yml",
"chars": 823,
"preview": "name: Close stale issues\non:\n schedule:\n - cron: \"30 1 * * *\"\n\njobs:\n close-issues:\n runs-on: ubuntu-latest\n "
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2737,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/release-build.yml",
"chars": 1264,
"preview": "on:\n release:\n types: [created]\n\nname: Build & release\njobs:\n releases-matrix:\n name: Build\n runs-on: ubuntu-"
},
{
"path": ".github/workflows/tests-rqlite.yml",
"chars": 877,
"preview": "name: Tests (rqlite)\non:\n pull_request:\n branches: [ develop, 'feature/**' ]\n push:\n branches: [ develop, 'featu"
},
{
"path": ".github/workflows/tests.yml",
"chars": 2000,
"preview": "name: Tests\non:\n pull_request:\n branches: [ develop, 'feature/**' ]\n push:\n branches: [ develop, 'feature/**' ]\n"
},
{
"path": ".gitignore",
"chars": 94,
"preview": "/node_modules/\n/send\n/sendmail/sendmail\n/server/ui/dist\n/Makefile\n/mailpit*\n/.idea\n*.old\n*.db\n"
},
{
"path": ".prettierignore",
"chars": 115,
"preview": "# Not within the scope of Prettier\n**/*.yml\n**/*.yaml\n**/*.json\n**/*.md\n**/*.css\n**/*.html\n**/*.scss\ncomposer.lock\n"
},
{
"path": ".vscode/settings.json",
"chars": 774,
"preview": "{\n \"[vue]\": {\n \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n },\n \"[javascript]\": {\n \"edito"
},
{
"path": "CHANGELOG.md",
"chars": 51565,
"preview": "# Changelog\n\nNotable changes to Mailpit will be documented in this file.\n\n## [v1.29.3]\n\n### Security\n- Enhance CORS orig"
},
{
"path": "CONTRIBUTING.md",
"chars": 1204,
"preview": "# Contributing to Mailpit\n\nThank you for your interest in contributing to Mailpit! \n\n## Reporting issues and feature req"
},
{
"path": "Dockerfile",
"chars": 935,
"preview": "FROM golang:alpine AS builder\n\nARG VERSION=dev\n\nCOPY . /app\n\nWORKDIR /app\n\nRUN apk upgrade && apk add git npm && \\\nnpm "
},
{
"path": "LICENSE",
"chars": 1085,
"preview": "The MIT License (MIT)\nCopyright (c) 2022-Now() Ralph Slooten\n\nPermission is hereby granted, free of charge, to any perso"
},
{
"path": "README.md",
"chars": 7469,
"preview": "<h1 align=\"center\">\n Mailpit - email testing for developers\n</h1>\n\n<div align=\"center\">\n <a href=\"https://github.com"
},
{
"path": "cmd/dump.go",
"chars": 1434,
"preview": "package cmd\n\nimport (\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/axllent/mailpit/internal/dump\"\n\t\"github.com/axll"
},
{
"path": "cmd/ingest.go",
"chars": 4253,
"preview": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/mail\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/axllent/"
},
{
"path": "cmd/readyz.go",
"chars": 2056,
"preview": "package cmd\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/axllent/mailpit/co"
},
{
"path": "cmd/reindex.go",
"chars": 864,
"preview": "package cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/axllent/mailpit/internal/logger\"\n\t\"github"
},
{
"path": "cmd/root.go",
"chars": 22599,
"preview": "// Package cmd is the main application\npackage cmd\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/c"
},
{
"path": "cmd/sendmail.go",
"chars": 1653,
"preview": "package cmd\n\nimport (\n\t\"os\"\n\n\tsendmail \"github.com/axllent/mailpit/sendmail/cmd\"\n\t\"github.com/spf13/cobra\"\n)\n\n// sendmai"
},
{
"path": "cmd/version.go",
"chars": 1614,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/spf13/cobra\"\n)\n\n// vers"
},
{
"path": "config/config.go",
"chars": 21938,
"preview": "// Package config handles the application configuration\npackage config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t"
},
{
"path": "config/tags.go",
"chars": 2427,
"preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/internal/logger\"\n\t\"githu"
},
{
"path": "config/utils.go",
"chars": 947,
"preview": "package config\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/internal/t"
},
{
"path": "config/validators.go",
"chars": 7866,
"preview": "package config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/mail\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.c"
},
{
"path": "esbuild.config.mjs",
"chars": 833,
"preview": "import * as esbuild from \"esbuild\";\nimport pluginVue from \"esbuild-plugin-vue-next\";\nimport { sassPlugin } from \"esbuild"
},
{
"path": "eslint.config.js",
"chars": 2245,
"preview": "import eslintConfigPrettier from \"eslint-config-prettier/flat\";\nimport globals from \"globals\";\nimport { includeIgnoreFil"
},
{
"path": "go.mod",
"chars": 3381,
"preview": "module github.com/axllent/mailpit\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/PuerkitoBio/goquery v1.11.0\n\tgithub.com/araddon/date"
},
{
"path": "go.sum",
"chars": 23918,
"preview": "github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=\ngithub.com"
},
{
"path": "install.sh",
"chars": 5969,
"preview": "#!/bin/sh\n\n# This script will install the latest release of Mailpit.\n\n# Check dependencies is installed\nfor cmd in curl "
},
{
"path": "internal/auth/auth.go",
"chars": 2196,
"preview": "// Package auth handles the web UI and SMTP authentication\npackage auth\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/tg1"
},
{
"path": "internal/dump/dump.go",
"chars": 3169,
"preview": "// Package dump is used to export all messages from mailpit into a directory\npackage dump\n\nimport (\n\t\"encoding/json\"\n\t\"e"
},
{
"path": "internal/html2text/html2text.go",
"chars": 2025,
"preview": "// Package html2text is a simple library to convert HTML to plain text\npackage html2text\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"reg"
},
{
"path": "internal/html2text/html2text_test.go",
"chars": 27774,
"preview": "package html2text\n\nimport \"testing\"\n\nfunc TestPlain(t *testing.T) {\n\ttests := map[string]string{}\n\ttests[\"this is a tes"
},
{
"path": "internal/htmlcheck/README.md",
"chars": 241,
"preview": "# HTML check\n\nThe database used for HTML support tests is based on [can I email](https://www.caniemail.com/).\n\nThe `cani"
},
{
"path": "internal/htmlcheck/caniemail-data.json",
"chars": 644461,
"preview": "{\n\t\"api_version\":\"1.0.4\",\n\t\"last_update_date\":\"2026-02-16 15:39:06 +0000\",\n\t\"nicenames\":{\"family\":{\"gmail\":\"Gmail\",\"outl"
},
{
"path": "internal/htmlcheck/caniemail.go",
"chars": 2004,
"preview": "// Package htmlcheck is used for parsing HTML and returning\n// HTML compatibility errors and warnings\npackage htmlcheck\n"
},
{
"path": "internal/htmlcheck/config.go",
"chars": 14930,
"preview": "package htmlcheck\n\nimport \"regexp\"\n\n// HTML tests\nvar htmlTests = map[string]string{\n\t// body check is manually done bec"
},
{
"path": "internal/htmlcheck/css.go",
"chars": 7670,
"preview": "package htmlcheck\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github"
},
{
"path": "internal/htmlcheck/html.go",
"chars": 2340,
"preview": "package htmlcheck\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\t\"github.com/axllent/mailpit/interna"
},
{
"path": "internal/htmlcheck/inline_test.go",
"chars": 4037,
"preview": "package htmlcheck\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n)\n\nfunc TestInlineSt"
},
{
"path": "internal/htmlcheck/main.go",
"chars": 5119,
"preview": "package htmlcheck\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\t\"github.com/axllent/"
},
{
"path": "internal/htmlcheck/platforms.go",
"chars": 798,
"preview": "package htmlcheck\n\nimport (\n\t\"slices\"\n\n\t\"github.com/axllent/mailpit/internal/tools\"\n)\n\n// Platforms returns all platform"
},
{
"path": "internal/htmlcheck/structs.go",
"chars": 2431,
"preview": "package htmlcheck\n\n// Response represents the HTML check response struct\n//\n// swagger:model HTMLCheckResponse\ntype Resp"
},
{
"path": "internal/linkcheck/linkcheck_test.go",
"chars": 2356,
"preview": "package linkcheck\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/axllent/mailpit/internal/storage\"\n)\n\nvar (\n\ttestHTML = `"
},
{
"path": "internal/linkcheck/main.go",
"chars": 2774,
"preview": "// Package linkcheck handles message links checking\npackage linkcheck\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/Puerk"
},
{
"path": "internal/linkcheck/status.go",
"chars": 3831,
"preview": "package linkcheck\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\""
},
{
"path": "internal/linkcheck/structs.go",
"chars": 420,
"preview": "package linkcheck\n\n// Response represents the Link check response\n//\n// swagger:model LinkCheckResponse\ntype Response st"
},
{
"path": "internal/logger/logger.go",
"chars": 1656,
"preview": "// Package logger handles the logging\npackage logger\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n"
},
{
"path": "internal/pop3/functions.go",
"chars": 1973,
"preview": "package pop3\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/internal/auth\"\n\t\"github.com/axll"
},
{
"path": "internal/pop3/pop3_test.go",
"chars": 7467,
"preview": "package pop3\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"math/rand/v2\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/axllent/"
},
{
"path": "internal/pop3/server.go",
"chars": 8913,
"preview": "// Package pop3 is a simple POP3 server for Mailpit.\n// By default it is disabled unless password credentials have been "
},
{
"path": "internal/pop3client/client.go",
"chars": 10405,
"preview": "// Package pop3client is borrowed directly from https://github.com/knadh/go-pop3 to reduce dependencies.\n// This is used"
},
{
"path": "internal/prometheus/metrics.go",
"chars": 5353,
"preview": "// Package prometheus provides Prometheus metrics for Mailpit\npackage prometheus\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"time"
},
{
"path": "internal/smtpd/chaos/chaos.go",
"chars": 3612,
"preview": "// Package chaos is used to simulate Chaos engineering (random failures) in the SMTPD server.\n// See https://en.wikipedi"
},
{
"path": "internal/smtpd/forward.go",
"chars": 4213,
"preview": "package smtpd\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/smtp\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github"
},
{
"path": "internal/smtpd/main.go",
"chars": 10263,
"preview": "// Package smtpd is the SMTP daemon\npackage smtpd\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/mail\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"g"
},
{
"path": "internal/smtpd/relay.go",
"chars": 5720,
"preview": "package smtpd\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/smtp\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github"
},
{
"path": "internal/smtpd/smtpd.go",
"chars": 32245,
"preview": "// Package smtpd implements a basic SMTP server.\n//\n// This is a modified version of https://github.com/mhale/smtpd to\n/"
},
{
"path": "internal/smtpd/smtpd_test.go",
"chars": 73092,
"preview": "package smtpd\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"err"
},
{
"path": "internal/snakeoil/snakeoil.go",
"chars": 4939,
"preview": "// Package snakeoil provides functionality to generate a temporary self-signed certificates\n// for testing purposes. It "
},
{
"path": "internal/spamassassin/postmark/postmark.go",
"chars": 2688,
"preview": "// Package postmark uses the free https://spamcheck.postmarkapp.com/\n// See https://spamcheck.postmarkapp.com/doc/ for m"
},
{
"path": "internal/spamassassin/spamassassin.go",
"chars": 3028,
"preview": "// Package spamassassin will return results from either a SpamAssassin server or\n// Postmark's public API depending on c"
},
{
"path": "internal/spamassassin/spamc/spamc.go",
"chars": 5564,
"preview": "// Package spamc provides a client for the SpamAssassin spamd protocol.\n// http://svn.apache.org/repos/asf/spamassassin/"
},
{
"path": "internal/stats/stats.go",
"chars": 4341,
"preview": "// Package stats stores and returns Mailpit statistics\npackage stats\n\nimport (\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/"
},
{
"path": "internal/storage/cron.go",
"chars": 4959,
"preview": "package storage\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\""
},
{
"path": "internal/storage/database.go",
"chars": 6400,
"preview": "// Package storage handles all database actions\npackage storage\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/s"
},
{
"path": "internal/storage/functions_test.go",
"chars": 1322,
"preview": "package storage\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/axllent/mailpit/int"
},
{
"path": "internal/storage/messages.go",
"chars": 19090,
"preview": "package storage\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\" // #nosec\n\t\"crypto/sha1\" // #nosec\n\t\"crypto/sha256\"\n\t\"datab"
},
{
"path": "internal/storage/messages_test.go",
"chars": 9185,
"preview": "package storage\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/axllent/mailpit/config\"\n)\n\nfunc TestTextEmailInserts(t "
},
{
"path": "internal/storage/notifications.go",
"chars": 739,
"preview": "package storage\n\nimport (\n\t\"time\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/axllent/mailpit/server/websockets\"\n"
},
{
"path": "internal/storage/reindex.go",
"chars": 3208,
"preview": "package storage\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/mail\"\n\t\"os\"\n\n\t\"github.com/ax"
},
{
"path": "internal/storage/schemas/1.0.0.sql",
"chars": 626,
"preview": "-- CREATE TABLES\nCREATE TABLE IF NOT EXISTS {{ tenant \"mailbox\" }} (\n\tSort INTEGER PRIMARY KEY AUTOINCREMENT,\n\tID TEXT N"
},
{
"path": "internal/storage/schemas/1.1.0.sql",
"chars": 186,
"preview": "-- CREATE TAGS COLUMN\nALTER TABLE {{ tenant \"mailbox\" }} ADD COLUMN Tags Text NOT NULL DEFAULT '[]';\nCREATE INDEX IF NOT"
},
{
"path": "internal/storage/schemas/1.2.0.sql",
"chars": 1608,
"preview": "-- CREATING NEW MAILBOX FORMAT\nCREATE TABLE IF NOT EXISTS {{ tenant \"mailboxtmp\" }} (\n\tCreated INTEGER NOT NULL,\n\tID TEX"
},
{
"path": "internal/storage/schemas/1.21.2.sql",
"chars": 213,
"preview": "-- DROP LEGACY MIGRATION TABLE\nDROP TABLE IF EXISTS {{ tenant \"darwin_migrations\" }};\n\n-- DROP LEGACY TAGS COLUMN\nDROP I"
},
{
"path": "internal/storage/schemas/1.21.8.sql",
"chars": 811,
"preview": "-- Rebuild message_tags to remove FOREIGN KEY REFERENCES\nPRAGMA foreign_keys=OFF;\n\nDROP INDEX IF EXISTS {{ tenant \"idx_m"
},
{
"path": "internal/storage/schemas/1.23.0.sql",
"chars": 236,
"preview": "-- CREATE Compressed COLUMN IN mailbox_data\nALTER TABLE {{ tenant \"mailbox_data\" }} ADD COLUMN Compressed INTEGER NOT NU"
},
{
"path": "internal/storage/schemas/1.3.0.sql",
"chars": 105,
"preview": "-- CREATE SNIPPET COLUMN\nALTER TABLE {{ tenant \"mailbox\" }} ADD COLUMN Snippet TEXT NOT NULL DEFAULT '';\n"
},
{
"path": "internal/storage/schemas/1.4.0.sql",
"chars": 650,
"preview": "-- CREATE TAG TABLES\nCREATE TABLE IF NOT EXISTS {{ tenant \"tags\" }} (\n\tID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n\tN"
},
{
"path": "internal/storage/schemas/1.5.0.sql",
"chars": 327,
"preview": "-- CREATE SETTINGS TABLE\nCREATE TABLE IF NOT EXISTS {{ tenant \"settings\" }} (\n\tKey TEXT,\n\tValue TEXT\n);\nCREATE UNIQUE IN"
},
{
"path": "internal/storage/schemas/README.md",
"chars": 228,
"preview": "# Migration scripts\n\n- Scripts should be named using semver and have the `.sql` extension.\n- Inline comments should be p"
},
{
"path": "internal/storage/schemas.go",
"chars": 3455,
"preview": "package storage\n\nimport (\n\t\"bytes\"\n\t\"embed\"\n\t\"log\"\n\t\"path\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/axllent/mai"
},
{
"path": "internal/storage/search.go",
"chars": 14721,
"preview": "package storage\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\""
},
{
"path": "internal/storage/search_test.go",
"chars": 6054,
"preview": "package storage\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"math/rand/v2\"\n\t\"testing\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/"
},
{
"path": "internal/storage/settings.go",
"chars": 2121,
"preview": "package storage\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/axllent/mailpit/internal/logger\"\n\t\"github.com/leporo/"
},
{
"path": "internal/storage/structs.go",
"chars": 3349,
"preview": "package storage\n\nimport (\n\t\"net/mail\"\n\t\"time\"\n)\n\n// Message data excluding physical attachments\n//\n// swagger:model Mess"
},
{
"path": "internal/storage/tagfilters.go",
"chars": 2161,
"preview": "package storage\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/axlle"
},
{
"path": "internal/storage/tags.go",
"chars": 9636,
"preview": "package storage\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/"
},
{
"path": "internal/storage/tags_test.go",
"chars": 4846,
"preview": "package storage\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"gith"
},
{
"path": "internal/storage/testdata/inline-attachment.eml",
"chars": 574,
"preview": "From: sender@example.com\nTo: recipient@example.com\nSubject: Test inline image proper\nMIME-Version: 1.0\nContent-Type: mul"
},
{
"path": "internal/storage/testdata/mime-attachment.eml",
"chars": 41424,
"preview": "Delivered-To: recipient2@example.com\r\nReceived: by 2002:a0c:fe87:0:0:0:0:0 with SMTP id d7csp145570qvs;\r\n Tue, 26"
},
{
"path": "internal/storage/testdata/mixed-attachment.eml",
"chars": 855,
"preview": "From: sender@example.com\nTo: recipient@example.com\nSubject: Test mixed attachments\nMIME-Version: 1.0\nContent-Type: multi"
},
{
"path": "internal/storage/testdata/plain-text.eml",
"chars": 7648,
"preview": "Delivered-To: recipient@example.com\r\nReceived: by 2002:a0c:fe87:0:0:0:0:0 with SMTP id d7csp146390qvs;\r\n Tue, 26 "
},
{
"path": "internal/storage/testdata/regular-attachment.eml",
"chars": 548,
"preview": "From: sender@example.com\nTo: recipient@example.com\nSubject: Test regular attachment\nMIME-Version: 1.0\nContent-Type: mult"
},
{
"path": "internal/storage/testdata/tags.eml",
"chars": 3060,
"preview": "Date: Wed, 27 Jul 2022 15:44:41 +1200\r\nFrom: Sender Smith <sender+FromFag@example.com>\r\nTo: Recipient Ross <recipient+To"
},
{
"path": "internal/storage/utils.go",
"chars": 2848,
"preview": "package storage\n\nimport (\n\t\"net/mail\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/axllent/mailpit/internal/html2tex"
},
{
"path": "internal/tools/argsparser.go",
"chars": 670,
"preview": "package tools\n\nimport \"strings\"\n\n// ArgsParser will split a string by new words and quotes phrases\nfunc ArgsParser(s str"
},
{
"path": "internal/tools/fs.go",
"chars": 431,
"preview": "package tools\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// IsFile returns whether a file exists and is readable\nfunc IsFile(pa"
},
{
"path": "internal/tools/headers.go",
"chars": 4746,
"preview": "// Package tools provides various methods for various things\npackage tools\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"net/mail\"\n\t\"reg"
},
{
"path": "internal/tools/html.go",
"chars": 1095,
"preview": "package tools\n\nimport (\n\t\"fmt\"\n\n\t\"golang.org/x/net/html\"\n)\n\n// GetHTMLAttributeVal returns the value of an HTML Attribut"
},
{
"path": "internal/tools/listunsubscribeparser.go",
"chars": 2530,
"preview": "package tools\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// ListUnsubscribeParser will attempt to parse a `List"
},
{
"path": "internal/tools/net.go",
"chars": 940,
"preview": "package tools\n\nimport (\n\t\"net\"\n\t\"net/url\"\n)\n\n// IsInternalIP checks if the given IP address is an internal IP address (e"
},
{
"path": "internal/tools/snippets.go",
"chars": 1675,
"preview": "package tools\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/internal/html2text\"\n)\n\n// CreateSnippet retur"
},
{
"path": "internal/tools/tags.go",
"chars": 1024,
"preview": "package tools\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\nvar (\n\t// Inva"
},
{
"path": "internal/tools/tools_test.go",
"chars": 5991,
"preview": "package tools\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestArgsParser(t *testing.T) {\n\ttests := map[string][]string{}\n\tte"
},
{
"path": "internal/tools/unixsocket.go",
"chars": 995,
"preview": "package tools\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"strconv\"\n)\n\n// UnixSocket returns a path and a "
},
{
"path": "internal/tools/utils.go",
"chars": 1159,
"preview": "package tools\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// Plural returns a singular or plural of a word together with th"
},
{
"path": "main.go",
"chars": 444,
"preview": "// Package main is the entrypoint\npackage main\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit"
},
{
"path": "package.json",
"chars": 1529,
"preview": "{\n \"name\": \"mailpit\",\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"private\": true,\n \"scripts\": {\n \"build\": \"MINIFY="
},
{
"path": "sendmail/cmd/cmd.go",
"chars": 5561,
"preview": "// Package cmd is the sendmail cli\npackage cmd\n\n/**\n * Bare bones sendmail drop-in replacement borrowed from MailHog\n *\n"
},
{
"path": "sendmail/cmd/smtp.go",
"chars": 2946,
"preview": "// Package cmd is a wrapper library to send mail\npackage cmd\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/mail\""
},
{
"path": "sendmail/main.go",
"chars": 91,
"preview": "package main\n\nimport \"github.com/axllent/mailpit/sendmail/cmd\"\n\nfunc main() {\n\tcmd.Run()\n}\n"
},
{
"path": "server/apiv1/api.go",
"chars": 2224,
"preview": "// Package apiv1 handles all the API responses\npackage apiv1\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t"
},
{
"path": "server/apiv1/application.go",
"chars": 2473,
"preview": "package apiv1\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/axllent/m"
},
{
"path": "server/apiv1/chaos.go",
"chars": 2028,
"preview": "package apiv1\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/axllent/mailpit/internal/smtpd/chaos\"\n)\n\n// GetChaos "
},
{
"path": "server/apiv1/message.go",
"chars": 4453,
"preview": "package apiv1\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/mail\"\n\t\"net/url\"\n\n\t\"github.com/axllent/mailpi"
},
{
"path": "server/apiv1/messages.go",
"chars": 6939,
"preview": "package apiv1\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/internal/storage\"\n\t\"github"
},
{
"path": "server/apiv1/other.go",
"chars": 3955,
"preview": "package apiv1\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/"
},
{
"path": "server/apiv1/release.go",
"chars": 4312,
"preview": "package apiv1\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/mail\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/axllent/mailp"
},
{
"path": "server/apiv1/send.go",
"chars": 4743,
"preview": "package apiv1\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/mail\"\n\t\"strin"
},
{
"path": "server/apiv1/structs.go",
"chars": 384,
"preview": "package apiv1\n\nimport (\n\t\"github.com/axllent/mailpit/internal/storage\"\n)\n\n// The following structs & aliases are provide"
},
{
"path": "server/apiv1/swagger-config.yml",
"chars": 418,
"preview": "consumes:\n - application/json\ninfo:\n description: |-\n OpenAPI 2.0 documentation for [Mailpit](https://github.com/a"
},
{
"path": "server/apiv1/swaggerParams.go",
"chars": 9772,
"preview": "// Package apiv1 provides the API v1 endpoints for Mailpit.\n//\n// These structs are for the purpose of defining swagger "
},
{
"path": "server/apiv1/swaggerResponses.go",
"chars": 3466,
"preview": "// Package apiv1 provides the API v1 endpoints for Mailpit.\n//\n// These structs are for the purpose of defining swagger "
},
{
"path": "server/apiv1/tags.go",
"chars": 3206,
"preview": "package apiv1\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/axllent/mailpit/internal/storage\"\n\t\"github.com/axllen"
},
{
"path": "server/apiv1/testing.go",
"chars": 4580,
"preview": "package apiv1\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github."
},
{
"path": "server/apiv1/thumbnails.go",
"chars": 3347,
"preview": "package apiv1\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"image/jpeg\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\""
},
{
"path": "server/cors.go",
"chars": 3598,
"preview": "package server\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/internal/logger\"\n)\n\nvar"
},
{
"path": "server/cors_test.go",
"chars": 3689,
"preview": "package server\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n)\n\nfunc TestExtractOrigins(t *testing.T) {\n\ttests := []struct {\n\t\tname "
},
{
"path": "server/embed.go",
"chars": 2196,
"preview": "package server\n\nimport (\n\t\"embed\"\n\t\"net/http\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/config\"\n)\n\nvar (\n\t//go:em"
},
{
"path": "server/handlers/k8healthz.go",
"chars": 169,
"preview": "package handlers\n\nimport \"net/http\"\n\n// HealthzHandler is a liveness probe\nfunc HealthzHandler(w http.ResponseWriter, _ "
},
{
"path": "server/handlers/k8sready.go",
"chars": 522,
"preview": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"sync/atomic\"\n\n\t\"github.com/axllent/mailpit/internal/storage\"\n)\n\n// ReadyzHandle"
},
{
"path": "server/handlers/messages.go",
"chars": 884,
"preview": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/axllent/mailpit/config\"\n\t\"github.com/axllent/"
},
{
"path": "server/handlers/proxy.go",
"chars": 10630,
"preview": "// Package handlers contains a specific handlers\npackage handlers\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n"
},
{
"path": "server/server.go",
"chars": 14991,
"preview": "// Package server is the HTTP daemon\npackage server\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t"
},
{
"path": "server/server_test.go",
"chars": 21351,
"preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"stri"
},
{
"path": "server/ui/api/v1/index.html",
"chars": 1393,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width,init"
},
{
"path": "server/ui/api/v1/swagger.json",
"chars": 63014,
"preview": "{\n \"consumes\": [\n \"application/json\"\n ],\n \"produces\": [\n \"application/json\"\n ],\n \"schemes\": [\n \"http\"\n ],"
},
{
"path": "server/ui-src/App.vue",
"chars": 974,
"preview": "<script>\nimport CommonMixins from \"./mixins/CommonMixins\";\nimport Favicon from \"./components/AppFavicon.vue\";\nimport App"
},
{
"path": "server/ui-src/app.js",
"chars": 507,
"preview": "import App from \"./App.vue\";\nimport router from \"./router\";\nimport { createApp } from \"vue\";\nimport mitt from \"mitt\";\n\ni"
},
{
"path": "server/ui-src/assets/_bootstrap.scss",
"chars": 1554,
"preview": "@import \"_bootstrap_variables\";\n\n// scss-docs-start import-stack\n// Configuration\n@import \"bootstrap/scss/functions\";\n@i"
},
{
"path": "server/ui-src/assets/_bootstrap_variables.scss",
"chars": 574,
"preview": "// Removed \"Noto Color Emoji\" from list re: https://github.com/axllent/mailpit/issues/92\n$font-family-sans-serif:\n sy"
},
{
"path": "server/ui-src/assets/styles.scss",
"chars": 6147,
"preview": "@import \"./bootstrap\";\n\n[v-cloak] {\n\tdisplay: none !important;\n}\n\n.navbar {\n\tz-index: 99;\n\n\t.navbar-brand {\n\t\tcolor: #2d"
},
{
"path": "server/ui-src/components/AjaxLoader.vue",
"chars": 375,
"preview": "<script>\nexport default {\n\tprops: {\n\t\tloading: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\t},\n};\n</script>\n\n<template>\n\t<div"
},
{
"path": "server/ui-src/components/AppAbout.vue",
"chars": 8282,
"preview": "<script>\nimport AjaxLoader from \"./AjaxLoader.vue\";\nimport Settings from \"./AppSettings.vue\";\nimport CommonMixins from \""
},
{
"path": "server/ui-src/components/AppBadge.vue",
"chars": 931,
"preview": "<script>\nimport { mailbox } from \"../stores/mailbox.js\";\n\nexport default {\n\tdata() {\n\t\treturn {\n\t\t\tupdating: false,\n\t\t\tn"
},
{
"path": "server/ui-src/components/AppFavicon.vue",
"chars": 2435,
"preview": "<script>\nimport { mailbox } from \"../stores/mailbox.js\";\n\nexport default {\n\tdata() {\n\t\treturn {\n\t\t\tfavicon: false,\n\t\t\tic"
},
{
"path": "server/ui-src/components/AppNotifications.vue",
"chars": 8224,
"preview": "<script>\nimport CommonMixins from \"../mixins/CommonMixins\";\nimport { Toast } from \"bootstrap\";\nimport { mailbox } from \""
},
{
"path": "server/ui-src/components/AppSettings.vue",
"chars": 13241,
"preview": "<script>\nimport CommonMixins from \"../mixins/CommonMixins\";\nimport Tags from \"bootstrap5-tags\";\nimport timezones from \"t"
},
{
"path": "server/ui-src/components/EditTags.vue",
"chars": 3500,
"preview": "<script>\nimport CommonMixins from \"../mixins/CommonMixins\";\nimport { mailbox } from \"../stores/mailbox\";\n\nexport default"
},
{
"path": "server/ui-src/components/ListMessages.vue",
"chars": 5217,
"preview": "<script>\nimport { mailbox } from \"../stores/mailbox\";\nimport CommonMixins from \"../mixins/CommonMixins\";\nimport dayjs fr"
},
{
"path": "server/ui-src/components/NavMailbox.vue",
"chars": 5256,
"preview": "<script>\nimport NavSelected from \"../components/NavSelected.vue\";\nimport AjaxLoader from \"./AjaxLoader.vue\";\nimport Comm"
},
{
"path": "server/ui-src/components/NavPagination.vue",
"chars": 2541,
"preview": "<script>\nimport CommonMixins from \"../mixins/CommonMixins\";\nimport { mailbox } from \"../stores/mailbox\";\nimport { limitO"
},
{
"path": "server/ui-src/components/NavSearch.vue",
"chars": 5618,
"preview": "<script>\nimport NavSelected from \"../components/NavSelected.vue\";\nimport AjaxLoader from \"./AjaxLoader.vue\";\nimport Comm"
},
{
"path": "server/ui-src/components/NavSelected.vue",
"chars": 2773,
"preview": "<script>\nimport AjaxLoader from \"./AjaxLoader.vue\";\nimport CommonMixins from \"../mixins/CommonMixins\";\nimport { mailbox "
},
{
"path": "server/ui-src/components/NavTags.vue",
"chars": 3043,
"preview": "<script>\nimport CommonMixins from \"../mixins/CommonMixins\";\nimport { mailbox } from \"../stores/mailbox\";\nimport { pagina"
},
{
"path": "server/ui-src/components/SearchForm.vue",
"chars": 1895,
"preview": "<script>\nimport CommonMixins from \"../mixins/CommonMixins\";\nimport { pagination } from \"../stores/pagination\";\n\nexport d"
},
{
"path": "server/ui-src/components/message/HTMLCheck.vue",
"chars": 22249,
"preview": "<script>\nimport { VcDonut } from \"vue-css-donut-chart\";\nimport axios from \"axios\";\nimport commonMixins from \"../../mixin"
},
{
"path": "server/ui-src/components/message/LinkCheck.vue",
"chars": 12523,
"preview": "<script>\nimport axios from \"axios\";\nimport commonMixins from \"../../mixins/CommonMixins\";\n\nexport default {\n\tmixins: [co"
},
{
"path": "server/ui-src/components/message/MessageAttachments.vue",
"chars": 8119,
"preview": "<script>\nimport commonMixins from \"../../mixins/CommonMixins\";\nimport { mailbox } from \"../../stores/mailbox\";\nimport IC"
},
{
"path": "server/ui-src/components/message/MessageHeaders.vue",
"chars": 837,
"preview": "<script>\nimport commonMixins from \"../../mixins/CommonMixins\";\n\nexport default {\n\tmixins: [commonMixins],\n\n\tprops: {\n\t\tm"
},
{
"path": "server/ui-src/components/message/MessageItem.vue",
"chars": 25408,
"preview": "<script>\nimport Attachments from \"./MessageAttachments.vue\";\nimport Headers from \"./MessageHeaders.vue\";\nimport HTMLChec"
},
{
"path": "server/ui-src/components/message/MessageRelease.vue",
"chars": 7013,
"preview": "<script>\nimport AjaxLoader from \"../AjaxLoader.vue\";\nimport Tags from \"bootstrap5-tags\";\nimport commonMixins from \"../.."
},
{
"path": "server/ui-src/components/message/MessageScreenshot.vue",
"chars": 5438,
"preview": "<script>\nimport AjaxLoader from \"../AjaxLoader.vue\";\nimport CommonMixins from \"../../mixins/CommonMixins\";\nimport { domT"
},
{
"path": "server/ui-src/components/message/SpamAssassin.vue",
"chars": 9646,
"preview": "<script>\nimport { VcDonut } from \"vue-css-donut-chart\";\nimport axios from \"axios\";\nimport commonMixins from \"../../mixin"
},
{
"path": "server/ui-src/docs.js",
"chars": 18,
"preview": "import \"rapidoc\";\n"
},
{
"path": "server/ui-src/mixins/CommonMixins.js",
"chars": 7365,
"preview": "import axios from \"axios\";\nimport dayjs from \"dayjs\";\nimport ColorHash from \"color-hash\";\nimport { Modal, Offcanvas } fr"
},
{
"path": "server/ui-src/mixins/MessagesMixins.js",
"chars": 2354,
"preview": "import CommonMixins from \"./CommonMixins.js\";\nimport { mailbox } from \"../stores/mailbox.js\";\nimport { pagination } from"
},
{
"path": "server/ui-src/router/index.js",
"chars": 766,
"preview": "import { createRouter, createWebHistory } from \"vue-router\";\nimport MailboxView from \"../views/MailboxView.vue\";\nimport "
},
{
"path": "server/ui-src/stores/mailbox.js",
"chars": 3310,
"preview": "// State Management\n\nimport { reactive, watch } from \"vue\";\n\n// Parse and validate a string[] from localStorage, returni"
},
{
"path": "server/ui-src/stores/pagination.js",
"chars": 362,
"preview": "import { reactive } from \"vue\";\n\nexport const pagination = reactive({\n\tstart: 0, // pagination offset\n\tlimit: 50, // per"
},
{
"path": "server/ui-src/views/MailboxView.vue",
"chars": 6604,
"preview": "<script>\nimport About from \"../components/AppAbout.vue\";\nimport AjaxLoader from \"../components/AjaxLoader.vue\";\nimport C"
},
{
"path": "server/ui-src/views/MessageView.vue",
"chars": 20680,
"preview": "<script>\nimport AboutMailpit from \"../components/AppAbout.vue\";\nimport AjaxLoader from \"../components/AjaxLoader.vue\";\ni"
},
{
"path": "server/ui-src/views/NotFoundView.vue",
"chars": 643,
"preview": "<script>\nimport About from \"../components/AppAbout.vue\";\nimport CommonMixins from \"../mixins/CommonMixins\";\n\nexport defa"
},
{
"path": "server/ui-src/views/SearchView.vue",
"chars": 5217,
"preview": "<script>\nimport About from \"../components/AppAbout.vue\";\nimport AjaxLoader from \"../components/AjaxLoader.vue\";\nimport C"
},
{
"path": "server/webhook/webhook.go",
"chars": 2029,
"preview": "// Package webhook will optionally call a preconfigured endpoint\npackage webhook\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"n"
},
{
"path": "server/websockets/client.go",
"chars": 3636,
"preview": "// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.\n// Use of this source code is governed by a BSD-st"
},
{
"path": "server/websockets/hub.go",
"chars": 2476,
"preview": "// Package websockets is used to broadcast messages to connected clients\npackage websockets\n\nimport (\n\t\"encoding/json\"\n\t"
}
]
About this extraction
This page contains the full source code of the axllent/mailpit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 185 files (1.6 MB), approximately 547.8k tokens, and a symbol index with 595 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.