Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | 3906a08037 |
|
@ -1,2 +0,0 @@
|
|||
[doc.extern-map.registries]
|
||||
crates-io = "https://docs.rs/"
|
|
@ -1,97 +0,0 @@
|
|||
<style>
|
||||
.rustdoc { flex-wrap: wrap; }
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--border-primary: #cdcdcd;
|
||||
--border-secondary: #cdcdcd;
|
||||
--a-normal-text: #034575;
|
||||
--a-normal-underline: #bbb;
|
||||
--a-visited-underline: #707070;
|
||||
--a-hover-bg: #bfbfbf40;
|
||||
--a-active-text: #c00;
|
||||
--a-active-underline: #c00;
|
||||
--accent-primary: #0085f2;
|
||||
--accent-primary-engage: #0085f21a;
|
||||
--accent-secondary: #0085f2;
|
||||
--accent-tertiary: #0085f21a;
|
||||
color-scheme: light;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--border-primary: #858585;
|
||||
--border-secondary: #696969;
|
||||
--a-normal-text: #4db4ff;
|
||||
--a-normal-underline: #8b8b8b;
|
||||
--a-visited-underline: #707070;
|
||||
--a-hover-bg: #bfbfbf40;
|
||||
--a-active-text: #c00;
|
||||
--a-active-underline: #c00;
|
||||
--accent-primary: #5e9eff;
|
||||
--accent-primary-engage: #5e9eff1a;
|
||||
--accent-secondary: #5e9eff;
|
||||
--accent-tertiary: #0085f21a;
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
nav.main-nav {
|
||||
padding: 0rem 1rem;
|
||||
border: 0.1rem solid var(--border-secondary);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: 2px;
|
||||
padding: 10px 14px 10px 10px;
|
||||
margin-bottom: 10px;
|
||||
/*! width: 100%; */
|
||||
height: max-content;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
nav.main-nav * {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav.main-nav > ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 1rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
nav.main-nav > ul > li > a {
|
||||
padding: 1rem;
|
||||
}
|
||||
nav.main-nav a[href] {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: currentcolor;
|
||||
color: #034575;
|
||||
color: var(--a-normal-text);
|
||||
text-decoration-color: #707070;
|
||||
text-decoration-color: var(--accent-secondary);
|
||||
text-decoration-skip-ink: none;
|
||||
font-synthesis: none;
|
||||
font-feature-settings: "onum" 1;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: -apple-system,BlinkMacSystemFont,Roboto,Roboto Slab,Droid Serif,Segoe UI,system-ui,Arial,sans-serif;
|
||||
font-size: 1.125em;
|
||||
}
|
||||
nav.main-nav > ul > li > a:hover {
|
||||
outline: 0.1rem solid;
|
||||
outline-offset: -0.5rem;
|
||||
}
|
||||
a[href]:focus, a[href]:hover {
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
</style>
|
||||
<nav class="main-nav">
|
||||
<ul>
|
||||
<li><a href="https://git.meli.delivery/meli/mailpot">Gitea Repository</a></li>
|
||||
<li><a href="https://github.com/meli/mailpot">Github Repository</a></li>
|
||||
<li><a href="https://lists.meli.delivery/">Official Instance</a></li>
|
||||
<li><a href="https://meli.github.io/mailpot/lists/2/">Static Demo</a></li>
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,5 +0,0 @@
|
|||
ignore-not-existing: true
|
||||
branch: true
|
||||
output-type: html
|
||||
binary-path: ./target/debug/
|
||||
output-path: ./coverage/
|
|
@ -3,7 +3,6 @@ name: Build release binary
|
|||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SQLITE_BIN: /home/runner/.sqlite3/sqlite3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -27,24 +26,6 @@ jobs:
|
|||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- id: cache-sqlite3-bin
|
||||
name: Cache sqlite3 binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.sqlite3
|
||||
key: toolchain-sqlite3
|
||||
- if: ${{ steps.cache-sqlite3-bin.outputs.cache-hit != 'true' }}
|
||||
name: Download sqlite3 binary
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get install -y --quiet wget unzip
|
||||
mkdir -p /home/runner/.sqlite3
|
||||
cd /home/runner/.sqlite3
|
||||
wget "https://sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip"
|
||||
unzip sqlite-tools-linux-x86-3420000.zip
|
||||
mv sqlite-tools-linux-x86-3420000/* .
|
||||
rm -rf sqlite-tools-linux-x86-3420000*
|
||||
echo "SQLITE_BIN=$(pwd)/sqlite3" >> $GITHUB_ENV
|
||||
- id: cache-rustup
|
||||
name: Cache Rust toolchain
|
||||
uses: actions/cache@v3
|
||||
|
@ -82,15 +63,13 @@ jobs:
|
|||
EOF
|
||||
- name: Build binary
|
||||
run: |
|
||||
cargo build --release --bin mpot --bin mpot-gen --bin mpot-web -p mailpot-cli -p mailpot-archives -p mailpot-web
|
||||
mkdir artifacts
|
||||
mv target/*/release/* target/ || true
|
||||
mv target/release/* target/ || true
|
||||
mv target/mpot target/mpot-web target/mpot-gen artifacts/
|
||||
cargo build --release --bin mpot --bin mpot-gen -p mailpot-cli -p mpot-archives
|
||||
mv target/*/release/mailpot target/mailpot || true
|
||||
mv target/release/mailpot target/mailpot || true
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: artifacts
|
||||
path: target/mailpot
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
name: Code coverage
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SQLITE_BIN: /home/runner/.sqlite3/sqlite3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: [Tests]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
on-success:
|
||||
runs-on: ubuntu-latest #if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- id: cache-sqlite3-bin
|
||||
name: Cache sqlite3 binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.sqlite3
|
||||
key: toolchain-sqlite3
|
||||
- if: ${{ steps.cache-sqlite3-bin.outputs.cache-hit != 'true' }}
|
||||
name: Download sqlite3 binary
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get install -y --quiet wget unzip
|
||||
mkdir -p /home/runner/.sqlite3
|
||||
cd /home/runner/.sqlite3
|
||||
wget "https://sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip"
|
||||
unzip sqlite-tools-linux-x86-3420000.zip
|
||||
mv sqlite-tools-linux-x86-3420000/* .
|
||||
rm -rf sqlite-tools-linux-x86-3420000*
|
||||
echo "SQLITE_BIN=$(pwd)/sqlite3" >> $GITHUB_ENV
|
||||
- id: cache-rustup
|
||||
name: Cache Rust toolchain
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.rustup
|
||||
key: toolchain-grcov
|
||||
- id: cache-cargo
|
||||
name: Cache Cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo
|
||||
key: cargo-grcov
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all --all-features --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests -Cinstrument-coverage'
|
||||
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests -Cinstrument-coverage'
|
||||
- uses: actions-rs/grcov@v0.1
|
||||
with:
|
||||
config: .github/grcov.yml
|
||||
- name: Upload report artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: report
|
||||
path: coverage
|
||||
|
||||
deploy:
|
||||
# Add a dependency to the build job
|
||||
needs: on-success
|
||||
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
# Specify runner + deployment step
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'gh-pages'
|
||||
token: ${{ secrets.GRCOVGHPAGES }}
|
||||
- name: Download coverage data
|
||||
id: download
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: report
|
||||
path: coverage
|
||||
- name: 'Echo download path'
|
||||
run: echo ${{steps.download.outputs.download-path}}
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
- name: Push
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git show-ref
|
||||
git add coverage
|
||||
git commit -m "Update grcov report"
|
||||
git show-ref
|
||||
git branch --verbose
|
||||
git remote set-url origin "https://${{github.actor}}:${{ secrets.GRCOVGHPAGES }}@github.com/${{github.repository}}.git"
|
||||
git push
|
|
@ -1,100 +0,0 @@
|
|||
name: Build rustdoc for Github Pages
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SQLITE_BIN: /home/runner/.sqlite3/sqlite3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- id: cache-sqlite3-bin
|
||||
name: Cache sqlite3 binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.sqlite3
|
||||
key: toolchain-sqlite3
|
||||
- if: ${{ steps.cache-sqlite3-bin.outputs.cache-hit != 'true' }}
|
||||
name: Download sqlite3 binary
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get install -y --quiet wget unzip
|
||||
mkdir -p /home/runner/.sqlite3
|
||||
cd /home/runner/.sqlite3
|
||||
wget "https://sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip"
|
||||
unzip sqlite-tools-linux-x86-3420000.zip
|
||||
mv sqlite-tools-linux-x86-3420000/* .
|
||||
rm -rf sqlite-tools-linux-x86-3420000*
|
||||
echo "SQLITE_BIN=$(pwd)/sqlite3" >> $GITHUB_ENV
|
||||
- id: cache-rustup
|
||||
name: Cache Rust toolchain
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.rustup
|
||||
key: toolchain-grcov
|
||||
- id: cache-cargo
|
||||
name: Cache Cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo
|
||||
key: cargo-grcov
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- name: Make rustdocs
|
||||
run: |
|
||||
make rustdoc-nightly || make rustdoc
|
||||
rm -rf docs
|
||||
ls -R
|
||||
mv target/doc docs
|
||||
- name: Upload report artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
deploy-docs:
|
||||
needs: build-docs
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'gh-pages'
|
||||
token: ${{ secrets.GRCOVGHPAGES }}
|
||||
- name: Download docs
|
||||
id: download
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docs
|
||||
path: docs
|
||||
- name: 'Echo download path'
|
||||
run: echo ${{steps.download.outputs.download-path}}
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
- name: Push
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git show-ref
|
||||
git add docs
|
||||
git commit -m "Update rustdoc"
|
||||
git show-ref
|
||||
git branch --verbose
|
||||
git remote set-url origin "https://${{github.actor}}:${{ secrets.GRCOVGHPAGES }}@github.com/${{github.repository}}.git"
|
||||
git push
|
|
@ -3,7 +3,6 @@ name: Tests
|
|||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SQLITE_BIN: /home/runner/.sqlite3/sqlite3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -31,24 +30,6 @@ jobs:
|
|||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- id: cache-sqlite3-bin
|
||||
name: Cache sqlite3 binary
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/.sqlite3
|
||||
key: toolchain-sqlite3
|
||||
- if: ${{ steps.cache-sqlite3-bin.outputs.cache-hit != 'true' }}
|
||||
name: Download sqlite3 binary
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get install -y --quiet wget unzip
|
||||
mkdir -p /home/runner/.sqlite3
|
||||
cd /home/runner/.sqlite3
|
||||
wget "https://sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip"
|
||||
unzip sqlite-tools-linux-x86-3420000.zip
|
||||
mv sqlite-tools-linux-x86-3420000/* .
|
||||
rm -rf sqlite-tools-linux-x86-3420000*
|
||||
echo "SQLITE_BIN=$(pwd)/sqlite3" >> $GITHUB_ENV
|
||||
- id: cache-rustup
|
||||
name: Cache Rust toolchain
|
||||
uses: actions/cache@v3
|
||||
|
@ -95,20 +76,16 @@ jobs:
|
|||
- name: cargo test
|
||||
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
|
||||
run: |
|
||||
cargo test --all --no-fail-fast --all-features
|
||||
cargo test --all --no-fail-fast --all-features
|
||||
- name: cargo-sort
|
||||
if: success() || failure()
|
||||
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
|
||||
run: |
|
||||
cargo sort --check
|
||||
- name: rustfmt
|
||||
if: success() || failure()
|
||||
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
|
||||
run: |
|
||||
cargo fmt --check --all
|
||||
- name: clippy
|
||||
if: success() || failure()
|
||||
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
|
||||
run: |
|
||||
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
- name: rustdoc
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make rustdoc
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
manos@pitsidianak.is.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
102
CONTRIBUTING.md
102
CONTRIBUTING.md
|
@ -1,102 +0,0 @@
|
|||
# Contributing to mailpot
|
||||
|
||||
Contributions are welcome and encouraged.
|
||||
They can be anything from spelling corrections, art, documentation, or source code fixes & additions.
|
||||
If a source code contribution is correct, functional and follows the code style and feature goals of the rest of the project, it will be merged.
|
||||
|
||||
**Table of contents**:
|
||||
|
||||
- [Important links](#important-links)
|
||||
- [Developing environment](#developing-environment)
|
||||
- [Testing](#testing)
|
||||
- [How to submit changes](#how-to-submit-changes)
|
||||
- [Choosing what to work on](#choosing-what-to-work-on)
|
||||
- [How to request an enhancement, new features](#how-to-request-an-enhancement-new-features)
|
||||
- [Style Guide / Coding conventions](#style-guide--coding-conventions)
|
||||
- [Specific questions and answers](#specific-questions-and-answers)
|
||||
- [How do I include new images / icons?](#how-do-i-include-new-images--icons)
|
||||
|
||||
## Important links
|
||||
|
||||
- Main repository: <https://git.meli.delivery/meli/mailpot>
|
||||
- Bug/Issue tracker: <https://git.meli.delivery/meli/mailpot/issues>
|
||||
- Mailing list: <https://lists.meli.delivery/list/mailpot-general/>
|
||||
|
||||
To privately contact the repository's owner, check their commit history for their e-mail address.
|
||||
|
||||
<sup><sub><a href="#contributing-to-mailpot">back to top</a></sub></sup>
|
||||
|
||||
## Developing environment
|
||||
|
||||
You will need a UNIX-like operating system that is supported by Rust.
|
||||
You can install rust and cargo with the [`rustup`](https://rustup.rs) tool.
|
||||
|
||||
<sup><sub><a href="#contributing-to-mailpot">back to top</a></sub></sup>
|
||||
|
||||
## Testing
|
||||
|
||||
All tests can be executed with `cargo`.
|
||||
Run
|
||||
|
||||
```shell
|
||||
cargo test --all --no-fail-fast --all-features
|
||||
```
|
||||
|
||||
to run all tests.
|
||||
|
||||
<sup><sub><a href="#contributing-to-mailpot">back to top</a></sub></sup>
|
||||
|
||||
## How to submit changes
|
||||
|
||||
Use gitea's PR functionality.
|
||||
Alternatively, submit patches to the mailing list.
|
||||
|
||||
<sup><sub><a href="#contributing-to-mailpot">back to top</a></sub></sup>
|
||||
|
||||
## Choosing what to work on
|
||||
|
||||
You can find some tasks in the bug tracker.
|
||||
Additionally, tasks are annotated inside the source code with the keywords `FIXME`, `TODO` and others.
|
||||
For a list of all tags search for `[tag:`.
|
||||
For a list of all references search for `[ref:`.
|
||||
To find tag references you can use a text search tool of your choice such as `grep`, `ripgrep` or others.
|
||||
The CLI tool `tagref` can also be used:
|
||||
|
||||
```shell
|
||||
/path/to/mailpot $ tagref list-refs
|
||||
[ref:FIXME] @ ./src/module.rs:106
|
||||
[ref:FIXME] @ ./src/module.rs:867
|
||||
[ref:FIXME] @ ./src/module.rs:30
|
||||
[ref:TODO] @ ./src/where.rs:411
|
||||
...
|
||||
```
|
||||
|
||||
You can of course filter or sort them by tag:
|
||||
|
||||
```shell
|
||||
/path/to/mailpot $ tagref list-refs | grep TODO
|
||||
...
|
||||
/path/to/mailpot $ tagref list-refs | sort -u
|
||||
...
|
||||
```
|
||||
|
||||
<sup><sub><a href="#contributing-to-mailpot">back to top</a></sub></sup>
|
||||
|
||||
## How to request an enhancement, new features
|
||||
|
||||
Simply open a new issue on the bug tracker or post on the mailing list.
|
||||
|
||||
<sup><sub><a href="#contributing-to-mailpot">back to top</a></sub></sup>
|
||||
|
||||
## Style Guide / Coding conventions
|
||||
|
||||
All Rust code must be formatted by `rustfmt`, and pass clippy lints.
|
||||
|
||||
```shell
|
||||
cargo check --all-features --all --tests --examples --benches --bins
|
||||
cargo +nightly fmt --all || cargo fmt --all
|
||||
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
djhtml -i web/src/templates/* || printf "djhtml binary not found in PATH.\n"
|
||||
```
|
||||
|
||||
<sup><sub><a href="#contributing-to-mailpot">back to top</a></sub></sup>
|
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
|
@ -1,17 +1,10 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"mailpot",
|
||||
"mailpot-archives",
|
||||
"mailpot-cli",
|
||||
"mailpot-http",
|
||||
"mailpot-tests",
|
||||
"mailpot-web",
|
||||
"archive-http",
|
||||
"cli",
|
||||
"core",
|
||||
"rest-http",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
split-debuginfo = "unpacked"
|
||||
#[patch.crates-io]
|
||||
#structopt-derive = { git = "https://github.com/epilys/structopt-derive-manpage" }
|
||||
|
|
42
Makefile
42
Makefile
|
@ -1,46 +1,12 @@
|
|||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGOBIN = cargo
|
||||
CARGOSORTBIN = cargo-sort
|
||||
DJHTMLBIN = djhtml
|
||||
BLACKBIN = black
|
||||
PRINTF = /usr/bin/printf
|
||||
|
||||
HTML_FILES := $(shell find mailpot-web/src/templates -type f -print0 | tr '\0' ' ')
|
||||
PY_FILES := $(shell find . -type f -name '*.py' -print0 | tr '\0' ' ')
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
@$(CARGOBIN) check --all-features --all --tests --examples --benches --bins
|
||||
cargo check --all
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@$(CARGOBIN) +nightly fmt --all || $(CARGOBIN) fmt --all
|
||||
@OUT=$$($(CARGOSORTBIN) -w 2>&1) || $(PRINTF) "ERROR: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
|
||||
@OUT=$$($(DJHTMLBIN) $(HTML_FILES) 2>&1) || $(PRINTF) "ERROR: %s djhtml failed or binary not found in PATH.\n" "$$OUT"
|
||||
@OUT=$$($(BLACKBIN) -q $(PY_FILES) 2>&1) || $(PRINTF) "ERROR: %s black failed or binary not found in PATH.\n" "$$OUT"
|
||||
cargo fmt --all
|
||||
cargo sort -w || true
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@$(CARGOBIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test: check lint
|
||||
@$(CARGOBIN) nextest run --all --no-fail-fast --all-features
|
||||
|
||||
.PHONY: rustdoc
|
||||
rustdoc:
|
||||
@RUSTDOCFLAGS="--html-before-content ./.github/doc_extra.html" $(CARGOBIN) doc --workspace --all-features --no-deps --document-private-items
|
||||
|
||||
.PHONY: rustdoc-open
|
||||
rustdoc-open:
|
||||
@RUSTDOCFLAGS="--html-before-content ./.github/doc_extra.html" $(CARGOBIN) doc --workspace --all-features --no-deps --document-private-items --open
|
||||
|
||||
.PHONY: rustdoc-nightly
|
||||
rustdoc-nightly:
|
||||
@RUSTDOCFLAGS="--html-before-content ./.github/doc_extra.html" $(CARGOBIN) +nightly doc -Zrustdoc-map -Z rustdoc-scrape-examples --workspace --all-features --no-deps --document-private-items
|
||||
|
||||
.PHONY: rustdoc-nightly-open
|
||||
rustdoc-nightly-open:
|
||||
@RUSTDOCFLAGS="--html-before-content ./.github/doc_extra.html" $(CARGOBIN) +nightly doc -Zrustdoc-map -Z rustdoc-scrape-examples --workspace --all-features --no-deps --document-private-items --open
|
||||
cargo clippy --all
|
||||
|
|
150
README.md
150
README.md
|
@ -1,42 +1,20 @@
|
|||
# mailpot - mailing list manager
|
||||
# mailpot - WIP mailing list manager
|
||||
|
||||
[![Latest Version]][crates.io] [![Coverage]][grcov-rport] [![docs.rs]][rustdoc] ![Top Language] ![License]
|
||||
|
||||
[Latest Version]: https://img.shields.io/crates/v/mailpot.svg?color=white
|
||||
[crates.io]: https://crates.io/crates/mailpot
|
||||
[Top Language]: https://img.shields.io/github/languages/top/meli/mailpot?color=white&logo=rust&logoColor=black
|
||||
[License]: https://img.shields.io/github/license/meli/mailpot?color=white
|
||||
[docs.rs]: https://img.shields.io/docsrs/mailpot?color=white
|
||||
[rustdoc]: https://meli.github.io/mailpot/docs/mailpot/
|
||||
[Coverage]: https://img.shields.io/endpoint?color=white&url=https://meli.github.io/mailpot/coverage/coverage.json
|
||||
[grcov-rport]: https://meli.github.io/mailpot/coverage/
|
||||
|
||||
- Official hosted instance of `mailpot-web` crate: <https://lists.meli.delivery/>
|
||||
- Rendered rustdoc: <https://meli.github.io/mailpot/docs/mailpot/>
|
||||
- CLI manpage: [`mpot.1`](./docs/mpot.1) [Rendered](https://git.meli.delivery/meli/mailpot/src/branch/main/docs/mpot.1)
|
||||
|
||||
| ℹ️ Interested in contributing? Consult [`CONTRIBUTING.md`](./CONTRIBUTING.md). |
|
||||
| --- |
|
||||
|
||||
## crates:
|
||||
Crates:
|
||||
|
||||
- `core` the library
|
||||
- `cli` a command line tool to manage lists
|
||||
- `web` an `axum` based web server capable of serving archives and authenticating list owners and members
|
||||
- `archive-http` static web archive generation or with a dynamic http server
|
||||
- `rest-http` a REST http server to manage lists
|
||||
|
||||
## Features
|
||||
## Project goals
|
||||
|
||||
- easy setup
|
||||
- extensible through Rust API as a [library](./core)
|
||||
- basic management through [CLI tool](./cli/)
|
||||
- optional lightweight web archiver ([static](./archive-http/) and [dynamic](./web/))
|
||||
- useful for both **newsletters**, **communities** and for static **article comments**
|
||||
|
||||
## Roadmap
|
||||
|
||||
- extensible through Rust API as a library
|
||||
- extensible through HTTP REST API as an HTTP server, with webhooks
|
||||
- basic management through CLI
|
||||
- optional lightweight web archiver
|
||||
- useful for both newsletters, discussions, article comments
|
||||
|
||||
## Initial setup
|
||||
|
||||
|
@ -47,8 +25,8 @@ $ mkdir -p /home/user/.config/mailpot
|
|||
$ export MPOT_CONFIG=/home/user/.config/mailpot/config.toml
|
||||
$ cargo run --bin mpot -- sample-config > "$MPOT_CONFIG"
|
||||
$ # edit config and set database path e.g. "/home/user/.local/share/mailpot/mpot.db"
|
||||
$ cargo run --bin mpot -- -c "$MPOT_CONFIG" list-lists
|
||||
No lists found.
|
||||
$ cargo run --bin mpot -- -c "$MPOT_CONFIG" db-location
|
||||
/home/user/.local/share/mailpot/mpot.db
|
||||
```
|
||||
|
||||
This creates the database file in the configuration file as if you executed the following:
|
||||
|
@ -61,73 +39,27 @@ $ sqlite3 /home/user/.local/share/mailpot/mpot.db < ./core/src/schema.sql
|
|||
|
||||
```text
|
||||
% mpot help
|
||||
GNU Affero version 3 or later <https://www.gnu.org/licenses/>
|
||||
mailpot 0.1.0
|
||||
mini mailing list manager
|
||||
|
||||
Tool for mailpot mailing list management.
|
||||
USAGE:
|
||||
mpot [FLAGS] [OPTIONS] <SUBCOMMAND>
|
||||
|
||||
Usage: mpot [OPTIONS] <COMMAND>
|
||||
FLAGS:
|
||||
-d, --debug Activate debug mode
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
Commands:
|
||||
sample-config
|
||||
Prints a sample config file to STDOUT
|
||||
dump-database
|
||||
Dumps database data to STDOUT
|
||||
list-lists
|
||||
Lists all registered mailing lists
|
||||
list
|
||||
Mailing list management
|
||||
create-list
|
||||
Create new list
|
||||
post
|
||||
Post message from STDIN to list
|
||||
flush-queue
|
||||
Flush outgoing e-mail queue
|
||||
error-queue
|
||||
Mail that has not been handled properly end up in the error queue
|
||||
queue
|
||||
Mail that has not been handled properly end up in the error queue
|
||||
import-maildir
|
||||
Import a maildir folder into an existing list
|
||||
update-postfix-config
|
||||
Update postfix maps and master.cf (probably needs root permissions)
|
||||
print-postfix-config
|
||||
Print postfix maps and master.cf entry to STDOUT
|
||||
accounts
|
||||
All Accounts
|
||||
account-info
|
||||
Account info
|
||||
add-account
|
||||
Add account
|
||||
remove-account
|
||||
Remove account
|
||||
update-account
|
||||
Update account info
|
||||
repair
|
||||
Show and fix possible data mistakes or inconsistencies
|
||||
help
|
||||
Print this message or the help of the given subcommand(s)
|
||||
OPTIONS:
|
||||
-c, --config <config> Set config file
|
||||
|
||||
Options:
|
||||
-d, --debug
|
||||
Print logs
|
||||
|
||||
-c, --config <CONFIG>
|
||||
Configuration file to use
|
||||
|
||||
-q, --quiet
|
||||
Silence all output
|
||||
|
||||
-v, --verbose...
|
||||
Verbose mode (-v, -vv, -vvv, etc)
|
||||
|
||||
-t, --ts <TS>
|
||||
Debug log timestamp (sec, ms, ns, none)
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
-V, --version
|
||||
Print version
|
||||
SUBCOMMANDS:
|
||||
create-list Create new list
|
||||
db-location Prints database filesystem location
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
list Mailing list management
|
||||
list-lists Lists all registered mailing lists
|
||||
post Post message from STDIN to list
|
||||
```
|
||||
|
||||
### Receiving mail
|
||||
|
@ -194,8 +126,8 @@ TRACE - Received envelope to post: Envelope {
|
|||
TRACE - Is post related to list [#1 test] Test list <test@localhost>? false
|
||||
TRACE - Is post related to list [#2 test-announce] test announcements <test-announce@localhost>? true
|
||||
TRACE - Examining list "test announcements" <test-announce@localhost>
|
||||
TRACE - List subscriptions [
|
||||
ListSubscription {
|
||||
TRACE - List members [
|
||||
ListMembership {
|
||||
list: 2,
|
||||
address: "exxxxx@localhost",
|
||||
name: None,
|
||||
|
@ -211,9 +143,9 @@ TRACE - Running FixCRLF filter
|
|||
TRACE - Running PostRightsCheck filter
|
||||
TRACE - Running AddListHeaders filter
|
||||
TRACE - Running FinalizeRecipients filter
|
||||
TRACE - examining subscription ListSubscription { list: 2, address: "exxxxx@localhost", name: None, digest: false, hide_address: false, receive_duplicates: false, receive_own_posts: true, receive_confirmation: true, enabled: true }
|
||||
TRACE - subscription is submitter
|
||||
TRACE - subscription gets copy
|
||||
TRACE - examining member ListMembership { list: 2, address: "exxxxx@localhost", name: None, digest: false, hide_address: false, receive_duplicates: false, receive_own_posts: true, receive_confirmation: true, enabled: true }
|
||||
TRACE - member is submitter
|
||||
TRACE - Member gets copy
|
||||
TRACE - result Ok(
|
||||
Post {
|
||||
list: MailingList {
|
||||
|
@ -228,7 +160,7 @@ TRACE - result Ok(
|
|||
display_name: "Mxxxx Pxxxxxxxxxxxx",
|
||||
address_spec: "exxxxx@localhost",
|
||||
},
|
||||
subscriptions: 1,
|
||||
members: 1,
|
||||
bytes: 851,
|
||||
policy: None,
|
||||
to: [
|
||||
|
@ -263,7 +195,6 @@ let config = Configuration {
|
|||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
db_path: db_path.clone(),
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
administrators: vec!["myaddress@example.com".to_string()],
|
||||
};
|
||||
let db = Connection::open_or_create_db(config)?.trusted();
|
||||
|
||||
|
@ -274,26 +205,25 @@ let list_pk = db.create_list(MailingList {
|
|||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
archive_url: None,
|
||||
})?.pk;
|
||||
|
||||
db.set_list_post_policy(
|
||||
db.set_list_policy(
|
||||
PostPolicy {
|
||||
pk: 0,
|
||||
list: list_pk,
|
||||
announce_only: false,
|
||||
subscription_only: true,
|
||||
subscriber_only: true,
|
||||
approval_needed: false,
|
||||
open: false,
|
||||
no_subscriptions: false,
|
||||
custom: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
// Drop privileges; we can only process new e-mail and modify subscriptions from now on.
|
||||
let mut db = db.untrusted();
|
||||
// Drop privileges; we can only process new e-mail and modify memberships from now on.
|
||||
let db = db.untrusted();
|
||||
|
||||
assert_eq!(db.list_subscriptions(list_pk)?.len(), 0);
|
||||
assert_eq!(db.list_members(list_pk)?.len(), 0);
|
||||
assert_eq!(db.list_posts(list_pk, None)?.len(), 0);
|
||||
|
||||
// Process a subscription request e-mail
|
||||
|
@ -307,7 +237,7 @@ Message-ID: <1@example.com>
|
|||
let envelope = melib::Envelope::from_bytes(subscribe_bytes, None)?;
|
||||
db.post(&envelope, subscribe_bytes, /* dry_run */ false)?;
|
||||
|
||||
assert_eq!(db.list_subscriptions(list_pk)?.len(), 1);
|
||||
assert_eq!(db.list_members(list_pk)?.len(), 1);
|
||||
assert_eq!(db.list_posts(list_pk, None)?.len(), 0);
|
||||
|
||||
// Process a post
|
||||
|
@ -323,7 +253,7 @@ let envelope =
|
|||
melib::Envelope::from_bytes(post_bytes, None).expect("Could not parse message");
|
||||
db.post(&envelope, post_bytes, /* dry_run */ false)?;
|
||||
|
||||
assert_eq!(db.list_subscriptions(list_pk)?.len(), 1);
|
||||
assert_eq!(db.list_members(list_pk)?.len(), 1);
|
||||
assert_eq!(db.list_posts(list_pk, None)?.len(), 1);
|
||||
# Ok::<(), Error>(())
|
||||
```
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "mpot-archives"
|
||||
version = "0.1.0"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2018"
|
||||
license = "LICENSE"
|
||||
readme = "README.md"
|
||||
description = "mailing list manager"
|
||||
repository = "https://github.com/meli/mailpot"
|
||||
keywords = ["mail", "mailing-lists"]
|
||||
categories = ["email"]
|
||||
default-run = "mpot-archives"
|
||||
|
||||
[[bin]]
|
||||
name = "mpot-archives"
|
||||
path = "src/main.rs"
|
||||
required-features = ["warp"]
|
||||
|
||||
[[bin]]
|
||||
name = "mpot-gen"
|
||||
path = "src/gen.rs"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "^0.4", optional = true }
|
||||
lazy_static = "*"
|
||||
mailpot = { version = "0.1.0", path = "../core" }
|
||||
minijinja = { version = "0.31.0", features = ["source", ], optional = true }
|
||||
percent-encoding = { version = "^2.1", optional = true }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
||||
serde_json = "^1"
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
warp = { version = "^0.3", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["gen"]
|
||||
gen = ["dep:chrono", "dep:minijinja"]
|
||||
warp = ["dep:percent-encoding", "dep:tokio", "dep:warp"]
|
|
@ -9,8 +9,8 @@
|
|||
// 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 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,
|
||||
|
@ -25,14 +25,14 @@ use chrono::*;
|
|||
#[allow(dead_code)]
|
||||
/// Generate a calendar view of the given date's month.
|
||||
///
|
||||
/// Each vector element is an array of seven numbers representing weeks
|
||||
/// (starting on Sundays), and each value is the numeric date.
|
||||
/// Each vector element is an array of seven numbers representing weeks (starting on Sundays),
|
||||
/// and each value is the numeric date.
|
||||
/// A value of zero means a date that not exists in the current month.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use chrono::*;
|
||||
/// use mailpot_archives::cal::calendarize;
|
||||
/// use calendarize::calendarize;
|
||||
///
|
||||
/// let date = NaiveDate::parse_from_str("2021-01-02", "%Y-%m-%d").unwrap();
|
||||
/// // Week = [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
|
||||
|
@ -50,8 +50,8 @@ pub fn calendarize(date: NaiveDate) -> Vec<[u32; 7]> {
|
|||
|
||||
/// Generate a calendar view of the given date's month and offset.
|
||||
///
|
||||
/// Each vector element is an array of seven numbers representing weeks
|
||||
/// (starting on Sundays), and each value is the numeric date.
|
||||
/// Each vector element is an array of seven numbers representing weeks (starting on Sundays),
|
||||
/// and each value is the numeric date.
|
||||
/// A value of zero means a date that not exists in the current month.
|
||||
///
|
||||
/// Offset means the number of days from sunday.
|
||||
|
@ -60,7 +60,7 @@ pub fn calendarize(date: NaiveDate) -> Vec<[u32; 7]> {
|
|||
/// # Examples
|
||||
/// ```
|
||||
/// use chrono::*;
|
||||
/// use mailpot_archives::cal::calendarize_with_offset;
|
||||
/// use calendarize::calendarize_with_offset;
|
||||
///
|
||||
/// let date = NaiveDate::parse_from_str("2021-01-02", "%Y-%m-%d").unwrap();
|
||||
/// // Week = [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
|
|
@ -17,11 +17,186 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{fs::OpenOptions, io::Write};
|
||||
extern crate mailpot;
|
||||
use chrono::Datelike;
|
||||
|
||||
use mailpot::*;
|
||||
use mailpot_archives::utils::*;
|
||||
use minijinja::value::Value;
|
||||
mod cal;
|
||||
|
||||
pub use mailpot::models::*;
|
||||
pub use mailpot::*;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
use minijinja::{Environment, Error, Source, State};
|
||||
|
||||
use minijinja::value::{Object, Value};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TEMPLATES: Environment<'static> = {
|
||||
let mut env = Environment::new();
|
||||
env.add_function("calendarize", calendarize);
|
||||
env.set_source(Source::from_path("src/templates/"));
|
||||
|
||||
env
|
||||
};
|
||||
}
|
||||
|
||||
trait StripCarets {
|
||||
fn strip_carets(&self) -> &str;
|
||||
}
|
||||
|
||||
impl StripCarets for &str {
|
||||
fn strip_carets(&self) -> &str {
|
||||
let mut self_ref = self.trim();
|
||||
if self_ref.starts_with('<') && self_ref.ends_with('>') {
|
||||
self_ref = &self_ref[1..self_ref.len().saturating_sub(1)];
|
||||
}
|
||||
self_ref
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MailingList {
|
||||
pub pk: i64,
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub address: String,
|
||||
pub description: Option<String>,
|
||||
pub archive_url: Option<String>,
|
||||
pub inner: DbVal<mailpot::models::MailingList>,
|
||||
}
|
||||
|
||||
impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||
fn from(val: DbVal<mailpot::models::MailingList>) -> Self {
|
||||
let DbVal(
|
||||
mailpot::models::MailingList {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
},
|
||||
_,
|
||||
) = val.clone();
|
||||
|
||||
Self {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
inner: val,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MailingList {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
self.id.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for MailingList {
|
||||
fn kind(&self) -> minijinja::value::ObjectKind {
|
||||
minijinja::value::ObjectKind::Struct(self)
|
||||
}
|
||||
|
||||
fn call_method(
|
||||
&self,
|
||||
_state: &State,
|
||||
name: &str,
|
||||
_args: &[Value],
|
||||
) -> std::result::Result<Value, Error> {
|
||||
match name {
|
||||
"subscribe_mailto" => Ok(Value::from_serializable(&self.inner.subscribe_mailto())),
|
||||
"unsubscribe_mailto" => Ok(Value::from_serializable(&self.inner.unsubscribe_mailto())),
|
||||
_ => Err(Error::new(
|
||||
minijinja::ErrorKind::UnknownMethod,
|
||||
format!("aaaobject has no method named {name}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl minijinja::value::StructObject for MailingList {
|
||||
fn get_field(&self, name: &str) -> Option<Value> {
|
||||
match name {
|
||||
"pk" => Some(Value::from_serializable(&self.pk)),
|
||||
"name" => Some(Value::from_serializable(&self.name)),
|
||||
"id" => Some(Value::from_serializable(&self.id)),
|
||||
"address" => Some(Value::from_serializable(&self.address)),
|
||||
"description" => Some(Value::from_serializable(&self.description)),
|
||||
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
||||
Some(&["pk", "name", "id", "address", "description", "archive_url"][..])
|
||||
}
|
||||
}
|
||||
|
||||
fn calendarize(_state: &State, args: Value, hists: Value) -> std::result::Result<Value, Error> {
|
||||
use chrono::Month;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
macro_rules! month {
|
||||
($int:expr) => {{
|
||||
let int = $int;
|
||||
match int {
|
||||
1 => Month::January.name(),
|
||||
2 => Month::February.name(),
|
||||
3 => Month::March.name(),
|
||||
4 => Month::April.name(),
|
||||
5 => Month::May.name(),
|
||||
6 => Month::June.name(),
|
||||
7 => Month::July.name(),
|
||||
8 => Month::August.name(),
|
||||
9 => Month::September.name(),
|
||||
10 => Month::October.name(),
|
||||
11 => Month::November.name(),
|
||||
12 => Month::December.name(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}};
|
||||
}
|
||||
let month = args.as_str().unwrap();
|
||||
let hist = hists
|
||||
.get_item(&Value::from(month))?
|
||||
.as_seq()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| usize::try_from(v).unwrap())
|
||||
.collect::<Vec<usize>>();
|
||||
let sum: usize = hists
|
||||
.get_item(&Value::from(month))?
|
||||
.as_seq()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| usize::try_from(v).unwrap())
|
||||
.sum();
|
||||
let date = chrono::NaiveDate::parse_from_str(&format!("{}-01", month), "%F").unwrap();
|
||||
// Week = [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
|
||||
Ok(minijinja::context! {
|
||||
month_name => month!(date.month()),
|
||||
month => month,
|
||||
month_int => date.month() as usize,
|
||||
year => date.year(),
|
||||
weeks => cal::calendarize_with_offset(date, 1),
|
||||
hist => hist,
|
||||
sum => sum,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Crumb {
|
||||
pub label: Cow<'static, str>,
|
||||
pub url: Cow<'static, str>,
|
||||
}
|
||||
|
||||
fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
|
@ -98,8 +273,8 @@ fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
std::fs::create_dir_all(&lists_path)?;
|
||||
lists_path.push("index.html");
|
||||
|
||||
let list = db.list(list.pk)?.unwrap();
|
||||
let post_policy = db.list_post_policy(list.pk)?;
|
||||
let list = db.list(list.pk)?;
|
||||
let post_policy = db.list_policy(list.pk)?;
|
||||
let months = db.months(list.pk)?;
|
||||
let posts = db.list_posts(list.pk, None)?;
|
||||
let mut hist = months
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
extern crate mailpot;
|
||||
|
||||
pub use mailpot::models::*;
|
||||
pub use mailpot::*;
|
||||
|
||||
use minijinja::{Environment, Source};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use warp::Filter;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TEMPLATES: Environment<'static> = {
|
||||
let mut env = Environment::new();
|
||||
env.set_source(Source::from_path("src/templates/"));
|
||||
|
||||
env
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config_path = std::env::args()
|
||||
.nth(1)
|
||||
.expect("Expected configuration file path as first argument.");
|
||||
let conf = Configuration::from_file(config_path).unwrap();
|
||||
|
||||
let conf1 = conf.clone();
|
||||
let list_handler = warp::path!("lists" / i64).map(move |list_pk: i64| {
|
||||
let db = Connection::open_db(conf1.clone()).unwrap();
|
||||
let list = db.list(list_pk).unwrap();
|
||||
let months = db.months(list_pk).unwrap();
|
||||
let posts = db
|
||||
.list_posts(list_pk, None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|post| {
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.expect("Could not parse mail");
|
||||
minijinja::context! {
|
||||
pk => post.pk,
|
||||
list => post.list,
|
||||
subject => envelope.subject(),
|
||||
address=> post.address,
|
||||
message_id => post.message_id,
|
||||
message => post.message,
|
||||
timestamp => post.timestamp,
|
||||
datetime => post.datetime,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let context = minijinja::context! {
|
||||
title=> &list.name,
|
||||
list=> &list,
|
||||
months=> &months,
|
||||
posts=> posts,
|
||||
body=>&list.description.clone().unwrap_or_default(),
|
||||
};
|
||||
Ok(warp::reply::html(
|
||||
TEMPLATES
|
||||
.get_template("list.html")
|
||||
.unwrap()
|
||||
.render(context)
|
||||
.unwrap_or_else(|err| err.to_string()),
|
||||
))
|
||||
});
|
||||
let conf2 = conf.clone();
|
||||
let post_handler =
|
||||
warp::path!("list" / i64 / String).map(move |list_pk: i64, message_id: String| {
|
||||
let message_id = percent_decode_str(&message_id).decode_utf8().unwrap();
|
||||
let db = Connection::open_db(conf2.clone()).unwrap();
|
||||
let list = db.list(list_pk).unwrap();
|
||||
let posts = db.list_posts(list_pk, None).unwrap();
|
||||
let post = posts
|
||||
.iter()
|
||||
.find(|p| message_id.contains(&p.message_id))
|
||||
.unwrap();
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.expect("Could not parse mail");
|
||||
let body = envelope.body_bytes(post.message.as_slice());
|
||||
let body_text = body.text();
|
||||
let context = minijinja::context !{
|
||||
title => &list.name,
|
||||
list => &list,
|
||||
post => &post,
|
||||
posts => &posts,
|
||||
body => &body_text,
|
||||
from => &envelope.field_from_to_string(),
|
||||
date => &envelope.date_as_str(),
|
||||
to => &envelope.field_to_to_string(),
|
||||
subject => &envelope.subject(),
|
||||
in_reply_to => &envelope.in_reply_to_display().map(|r| r.to_string()),
|
||||
references => &envelope .references() .into_iter() .map(|m| m.to_string()) .collect::<Vec<String>>(),
|
||||
};
|
||||
Ok(warp::reply::html(
|
||||
TEMPLATES
|
||||
.get_template("post.html")
|
||||
.unwrap()
|
||||
.render(context)
|
||||
.unwrap_or_else(|err| err.to_string()),
|
||||
))
|
||||
});
|
||||
let conf3 = conf.clone();
|
||||
let index_handler = warp::path::end().map(move || {
|
||||
let db = Connection::open_db(conf3.clone()).unwrap();
|
||||
let lists_values = db.lists().unwrap();
|
||||
let lists = lists_values
|
||||
.iter()
|
||||
.map(|list| {
|
||||
let months = db.months(list.pk).unwrap();
|
||||
let posts = db.list_posts(list.pk, None).unwrap();
|
||||
minijinja::context! {
|
||||
title => &list.name,
|
||||
list => &list,
|
||||
posts => &posts,
|
||||
months => &months,
|
||||
body => &list.description.as_deref().unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let context = minijinja::context! {
|
||||
title => "mailing list archive",
|
||||
description => "",
|
||||
lists => &lists,
|
||||
};
|
||||
Ok(warp::reply::html(
|
||||
TEMPLATES
|
||||
.get_template("lists.html")
|
||||
.unwrap()
|
||||
.render(context)
|
||||
.unwrap_or_else(|err| err.to_string()),
|
||||
))
|
||||
});
|
||||
let routes = warp::get()
|
||||
.and(index_handler)
|
||||
.or(list_handler)
|
||||
.or(post_handler);
|
||||
|
||||
// Note that composing filters for many routes may increase compile times (because it uses a lot of generics).
|
||||
// If you wish to use dynamic dispatch instead and speed up compile times while
|
||||
// making it slightly slower at runtime, you can use Filter::boxed().
|
||||
|
||||
eprintln!("Running at http://127.0.0.1:3030");
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
|
||||
}
|
|
@ -7,31 +7,31 @@
|
|||
{% else %}
|
||||
{% if not post_policy.no_subscriptions %}
|
||||
<h2 id="subscribe">Subscribe</h2>
|
||||
{% set subscription_mailto=list.subscription_mailto() %}
|
||||
{% if subscription_mailto %}
|
||||
{% if subscription_mailto.subject %}
|
||||
{% set subscribe_mailto=list.subscribe_mailto() %}
|
||||
{% if subscribe_mailto %}
|
||||
{% if subscribe_mailto.subject %}
|
||||
<p>
|
||||
<a href="mailto:{{ subscription_mailto.address|safe }}?subject={{ subscription_mailto.subject|safe }}"><code>{{ subscription_mailto.address }}</code></a> with the following subject: <code>{{ subscription_mailto.subject}}</code>
|
||||
<a href="mailto:{{ subscribe_mailto.address|safe }}?subject={{ subscribe_mailto.subject|safe }}"><code>{{ subscribe_mailto.address }}</code></a> with the following subject: <code>{{ subscribe_mailto.subject}}</code>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="mailto:{{ subscription_mailto.address|safe }}"><code>{{ subscription_mailto.address }}</code></a>
|
||||
<a href="mailto:{{ subscribe_mailto.address|safe }}"><code>{{ subscribe_mailto.address }}</code></a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>List is not open for subscriptions.</p>
|
||||
{% endif %}
|
||||
|
||||
{% set unsubscription_mailto=list.unsubscription_mailto() %}
|
||||
{% if unsubscription_mailto %}
|
||||
{% set unsubscribe_mailto=list.unsubscribe_mailto() %}
|
||||
{% if unsubscribe_mailto %}
|
||||
<h2 id="unsubscribe">Unsubscribe</h2>
|
||||
{% if unsubscription_mailto.subject %}
|
||||
{% if unsubscribe_mailto.subject %}
|
||||
<p>
|
||||
<a href="mailto:{{ unsubscription_mailto.address|safe }}?subject={{ unsubscription_mailto.subject|safe }}"><code>{{ unsubscription_mailto.address }}</code></a> with the following subject: <code>{{unsubscription_mailto.subject}}</code>
|
||||
<a href="mailto:{{ unsubscribe_mailto.address|safe }}?subject={{ unsubscribe_mailto.subject|safe }}"><code>{{ unsubscribe_mailto.address }}</code></a> with the following subject: <code>{{unsubscribe_mailto.subject}}</code>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="mailto:{{ unsubscription_mailto.address|safe }}"><code>{{ unsubscription_mailto.address }}</code></a>
|
||||
<a href="mailto:{{ unsubscribe_mailto.address|safe }}"><code>{{ unsubscribe_mailto.address }}</code></a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -40,8 +40,8 @@
|
|||
<h2 id="post">Post</h2>
|
||||
{% if post_policy.announce_only %}
|
||||
<p>List is <em>announce-only</em>, i.e. you can only subscribe to receive announcements.</p>
|
||||
{% elif post_policy.subscription_only %}
|
||||
<p>List is <em>subscription-only</em>, i.e. you can only post if you are subscribed.</p>
|
||||
{% elif post_policy.subscriber_only %}
|
||||
<p>List is <em>subscriber-only</em>, i.e. you can only post if you are subscribed.</p>
|
||||
<p>If you are subscribed, you can send new posts to:
|
||||
<a href="mailto:{{ list.address| safe }}"><code>{{ list.address }}</code></a>
|
||||
</p>
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "mailpot-cli"
|
||||
version = "0.1.0"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2018"
|
||||
license = "LICENSE"
|
||||
readme = "README.md"
|
||||
description = "mailing list manager"
|
||||
repository = "https://github.com/meli/mailpot"
|
||||
keywords = ["mail", "mailing-lists"]
|
||||
categories = ["email"]
|
||||
default-run = "mpot"
|
||||
|
||||
[[bin]]
|
||||
name = "mpot"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
mailpot = { version = "0.1.0", path = "../core" }
|
||||
stderrlog = "^0.5"
|
||||
structopt = "0.3.16"
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//use std::io::Write;
|
||||
//use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
//println!("cargo:rerun-if-changed=../docs/command.mdoc");
|
||||
//println!("cargo:rerun-if-changed=../docs/list.mdoc");
|
||||
//println!("cargo:rerun-if-changed=../docs/error_queue.mdoc");
|
||||
//println!("cargo:rerun-if-changed=../docs/main.mdoc");
|
||||
//println!("cargo:rerun-if-changed=../docs/header.mdoc");
|
||||
//println!("cargo:rerun-if-changed=../docs/footer.mdoc");
|
||||
//println!("cargo:rerun-if-changed=../docs/mailpot.1.m4");
|
||||
//println!("cargo:rerun-if-changed=./src/main.rs");
|
||||
//println!("build running");
|
||||
//std::env::set_current_dir("..").expect("could not chdir('..')");
|
||||
|
||||
//let output = Command::new("m4")
|
||||
// .arg("./docs/mailpot.1.m4")
|
||||
// .output()
|
||||
// .unwrap();
|
||||
//let mut file = std::fs::File::create("./docs/mailpot.1").unwrap();
|
||||
//file.write_all(&output.stdout).unwrap();
|
||||
}
|
|
@ -0,0 +1,707 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
extern crate log;
|
||||
extern crate mailpot;
|
||||
extern crate stderrlog;
|
||||
|
||||
pub use mailpot::mail::*;
|
||||
pub use mailpot::models::changesets::*;
|
||||
pub use mailpot::models::*;
|
||||
pub use mailpot::*;
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
macro_rules! list {
|
||||
($db:ident, $list_id:expr) => {{
|
||||
$db.list_by_id(&$list_id)?.or_else(|| {
|
||||
$list_id
|
||||
.parse::<i64>()
|
||||
.ok()
|
||||
.map(|pk| $db.list(pk).ok())
|
||||
.flatten()
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(
|
||||
name = "mailpot",
|
||||
about = "mini mailing list manager",
|
||||
author = "Manos Pitsidianakis <epilys@nessuent.xyz>",
|
||||
//manpage = "docs/main.mdoc",
|
||||
//manpage_header = "docs/header.mdoc",
|
||||
//manpage_footer = "docs/footer.mdoc"
|
||||
)]
|
||||
struct Opt {
|
||||
/// Activate debug mode
|
||||
#[structopt(short, long)]
|
||||
debug: bool,
|
||||
|
||||
/// Set config file
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
config: PathBuf,
|
||||
#[structopt(flatten)]
|
||||
cmd: Command,
|
||||
/// Silence all output
|
||||
#[structopt(short = "q", long = "quiet")]
|
||||
quiet: bool,
|
||||
/// Verbose mode (-v, -vv, -vvv, etc)
|
||||
#[structopt(short = "v", long = "verbose", parse(from_occurrences))]
|
||||
verbose: usize,
|
||||
/// Timestamp (sec, ms, ns, none)
|
||||
#[structopt(short = "t", long = "timestamp")]
|
||||
ts: Option<stderrlog::Timestamp>,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
//#[structopt(manpage = "docs/command.mdoc")]
|
||||
enum Command {
|
||||
/// Prints a sample config file to STDOUT
|
||||
SampleConfig,
|
||||
///Dumps database data to STDOUT
|
||||
DumpDatabase,
|
||||
///Lists all registered mailing lists
|
||||
ListLists,
|
||||
///Mailing list management
|
||||
List {
|
||||
///Selects mailing list to operate on
|
||||
list_id: String,
|
||||
#[structopt(subcommand)]
|
||||
cmd: ListCommand,
|
||||
},
|
||||
///Create new list
|
||||
CreateList {
|
||||
///List name
|
||||
#[structopt(long)]
|
||||
name: String,
|
||||
///List ID
|
||||
#[structopt(long)]
|
||||
id: String,
|
||||
///List e-mail address
|
||||
#[structopt(long)]
|
||||
address: String,
|
||||
///List description
|
||||
#[structopt(long)]
|
||||
description: Option<String>,
|
||||
///List archive URL
|
||||
#[structopt(long)]
|
||||
archive_url: Option<String>,
|
||||
},
|
||||
///Post message from STDIN to list
|
||||
Post {
|
||||
#[structopt(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
/// Mail that has not been handled properly end up in the error queue.
|
||||
ErrorQueue {
|
||||
#[structopt(subcommand)]
|
||||
cmd: ErrorQueueCommand,
|
||||
},
|
||||
/// Import a maildir folder into an existing list.
|
||||
ImportMaildir {
|
||||
///Selects mailing list to operate on
|
||||
list_id: String,
|
||||
#[structopt(long, parse(from_os_str))]
|
||||
maildir_path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
//#[structopt(manpage = "docs/error_queue.mdoc")]
|
||||
enum ErrorQueueCommand {
|
||||
/// List.
|
||||
List,
|
||||
/// Print entry in RFC5322 or JSON format.
|
||||
Print {
|
||||
/// index of entry.
|
||||
#[structopt(long)]
|
||||
index: Vec<i64>,
|
||||
/// JSON format.
|
||||
#[structopt(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Delete entry and print it in stdout.
|
||||
Delete {
|
||||
/// index of entry.
|
||||
#[structopt(long)]
|
||||
index: Vec<i64>,
|
||||
/// Do not print in stdout.
|
||||
#[structopt(long)]
|
||||
quiet: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
//#[structopt(manpage = "docs/list.mdoc")]
|
||||
enum ListCommand {
|
||||
/// List members of list.
|
||||
Members,
|
||||
/// Add member to list.
|
||||
AddMember {
|
||||
/// E-mail address
|
||||
#[structopt(long)]
|
||||
address: String,
|
||||
/// Name
|
||||
#[structopt(long)]
|
||||
name: Option<String>,
|
||||
/// Send messages as digest?
|
||||
#[structopt(long)]
|
||||
digest: bool,
|
||||
/// Hide message from list when posting?
|
||||
#[structopt(long)]
|
||||
hide_address: bool,
|
||||
/// Hide message from list when posting?
|
||||
#[structopt(long)]
|
||||
/// Receive confirmation email when posting?
|
||||
receive_confirmation: Option<bool>,
|
||||
#[structopt(long)]
|
||||
/// Receive posts from list even if address exists in To or Cc header?
|
||||
receive_duplicates: Option<bool>,
|
||||
#[structopt(long)]
|
||||
/// Receive own posts from list?
|
||||
receive_own_posts: Option<bool>,
|
||||
#[structopt(long)]
|
||||
/// Is subscription enabled?
|
||||
enabled: Option<bool>,
|
||||
},
|
||||
/// Remove member from list.
|
||||
RemoveMember {
|
||||
#[structopt(long)]
|
||||
/// E-mail address
|
||||
address: String,
|
||||
},
|
||||
/// Update membership info.
|
||||
UpdateMembership {
|
||||
address: String,
|
||||
name: Option<String>,
|
||||
digest: Option<bool>,
|
||||
hide_address: Option<bool>,
|
||||
receive_duplicates: Option<bool>,
|
||||
receive_own_posts: Option<bool>,
|
||||
receive_confirmation: Option<bool>,
|
||||
enabled: Option<bool>,
|
||||
},
|
||||
/// Add policy to list.
|
||||
AddPolicy {
|
||||
#[structopt(long)]
|
||||
announce_only: bool,
|
||||
#[structopt(long)]
|
||||
subscriber_only: bool,
|
||||
#[structopt(long)]
|
||||
approval_needed: bool,
|
||||
#[structopt(long)]
|
||||
no_subscriptions: bool,
|
||||
#[structopt(long)]
|
||||
custom: bool,
|
||||
},
|
||||
RemovePolicy {
|
||||
#[structopt(long)]
|
||||
pk: i64,
|
||||
},
|
||||
/// Add list owner to list.
|
||||
AddListOwner {
|
||||
#[structopt(long)]
|
||||
address: String,
|
||||
#[structopt(long)]
|
||||
name: Option<String>,
|
||||
},
|
||||
RemoveListOwner {
|
||||
#[structopt(long)]
|
||||
pk: i64,
|
||||
},
|
||||
/// Alias for update-membership --enabled true
|
||||
EnableMembership { address: String },
|
||||
/// Alias for update-membership --enabled false
|
||||
DisableMembership { address: String },
|
||||
/// Update mailing list details.
|
||||
Update {
|
||||
name: Option<String>,
|
||||
id: Option<String>,
|
||||
address: Option<String>,
|
||||
description: Option<String>,
|
||||
archive_url: Option<String>,
|
||||
},
|
||||
/// Show mailing list health status.
|
||||
Health,
|
||||
/// Show mailing list info.
|
||||
Info,
|
||||
}
|
||||
|
||||
fn run_app(opt: Opt) -> Result<()> {
|
||||
if opt.debug {
|
||||
println!("DEBUG: {:?}", &opt);
|
||||
}
|
||||
if let Command::SampleConfig = opt.cmd {
|
||||
println!("{}", Configuration::new("/path/to/sqlite.db").to_toml());
|
||||
return Ok(());
|
||||
};
|
||||
let config = Configuration::from_file(opt.config.as_path())?;
|
||||
use Command::*;
|
||||
let mut db = Connection::open_or_create_db(config)?;
|
||||
match opt.cmd {
|
||||
SampleConfig => {}
|
||||
DumpDatabase => {
|
||||
let lists = db.lists()?;
|
||||
let mut stdout = std::io::stdout();
|
||||
serde_json::to_writer_pretty(&mut stdout, &lists)?;
|
||||
for l in &lists {
|
||||
serde_json::to_writer_pretty(&mut stdout, &db.list_members(l.pk)?)?;
|
||||
}
|
||||
}
|
||||
ListLists => {
|
||||
let lists = db.lists()?;
|
||||
if lists.is_empty() {
|
||||
println!("No lists found.");
|
||||
} else {
|
||||
for l in lists {
|
||||
println!("- {} {:?}", l.id, l);
|
||||
let list_owners = db.list_owners(l.pk)?;
|
||||
if list_owners.is_empty() {
|
||||
println!("\tList owners: None");
|
||||
} else {
|
||||
println!("\tList owners:");
|
||||
for o in list_owners {
|
||||
println!("\t- {}", o);
|
||||
}
|
||||
}
|
||||
if let Some(s) = db.list_policy(l.pk)? {
|
||||
println!("\tList policy: {}", s);
|
||||
} else {
|
||||
println!("\tList policy: None");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
List { list_id, cmd } => {
|
||||
let list = match list!(db, list_id) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(format!("No list with id or pk {} was found", list_id).into());
|
||||
}
|
||||
};
|
||||
use ListCommand::*;
|
||||
match cmd {
|
||||
Members => {
|
||||
let members = db.list_members(list.pk)?;
|
||||
if members.is_empty() {
|
||||
println!("No members found.");
|
||||
} else {
|
||||
println!("Members of list {}", list.id);
|
||||
for l in members {
|
||||
println!("- {}", &l);
|
||||
}
|
||||
}
|
||||
}
|
||||
AddMember {
|
||||
address,
|
||||
name,
|
||||
digest,
|
||||
hide_address,
|
||||
receive_confirmation,
|
||||
receive_duplicates,
|
||||
receive_own_posts,
|
||||
enabled,
|
||||
} => {
|
||||
db.add_member(
|
||||
list.pk,
|
||||
ListMembership {
|
||||
pk: 0,
|
||||
list: list.pk,
|
||||
name,
|
||||
address,
|
||||
digest,
|
||||
hide_address,
|
||||
receive_confirmation: receive_confirmation.unwrap_or(true),
|
||||
receive_duplicates: receive_duplicates.unwrap_or(true),
|
||||
receive_own_posts: receive_own_posts.unwrap_or(false),
|
||||
enabled: enabled.unwrap_or(true),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
RemoveMember { address } => {
|
||||
loop {
|
||||
println!(
|
||||
"Are you sure you want to remove membership of {} from list {}? [Yy/n]",
|
||||
address, list
|
||||
);
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
|
||||
break;
|
||||
} else if input.trim() == "n" {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
db.remove_membership(list.pk, &address)?;
|
||||
}
|
||||
Health => {
|
||||
println!("{} health:", list);
|
||||
let list_owners = db.list_owners(list.pk)?;
|
||||
let list_policy = db.list_policy(list.pk)?;
|
||||
if list_owners.is_empty() {
|
||||
println!("\tList has no owners: you should add at least one.");
|
||||
} else {
|
||||
for owner in list_owners {
|
||||
println!("\tList owner: {}.", owner);
|
||||
}
|
||||
}
|
||||
if let Some(list_policy) = list_policy {
|
||||
println!("\tList has post policy: {}.", list_policy);
|
||||
} else {
|
||||
println!("\tList has no post policy: you should add one.");
|
||||
}
|
||||
}
|
||||
Info => {
|
||||
println!("{} info:", list);
|
||||
let list_owners = db.list_owners(list.pk)?;
|
||||
let list_policy = db.list_policy(list.pk)?;
|
||||
let members = db.list_members(list.pk)?;
|
||||
if members.is_empty() {
|
||||
println!("No members.");
|
||||
} else if members.len() == 1 {
|
||||
println!("1 member.");
|
||||
} else {
|
||||
println!("{} members.", members.len());
|
||||
}
|
||||
if list_owners.is_empty() {
|
||||
println!("List owners: None");
|
||||
} else {
|
||||
println!("List owners:");
|
||||
for o in list_owners {
|
||||
println!("\t- {}", o);
|
||||
}
|
||||
}
|
||||
if let Some(s) = list_policy {
|
||||
println!("List policy: {}", s);
|
||||
} else {
|
||||
println!("List policy: None");
|
||||
}
|
||||
}
|
||||
UpdateMembership {
|
||||
address,
|
||||
name,
|
||||
digest,
|
||||
hide_address,
|
||||
receive_duplicates,
|
||||
receive_own_posts,
|
||||
receive_confirmation,
|
||||
enabled,
|
||||
} => {
|
||||
let name = if name
|
||||
.as_ref()
|
||||
.map(|s: &String| s.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
};
|
||||
let changeset = ListMembershipChangeset {
|
||||
list: list.pk,
|
||||
address,
|
||||
name,
|
||||
digest,
|
||||
hide_address,
|
||||
receive_duplicates,
|
||||
receive_own_posts,
|
||||
receive_confirmation,
|
||||
enabled,
|
||||
};
|
||||
db.update_member(changeset)?;
|
||||
}
|
||||
AddPolicy {
|
||||
announce_only,
|
||||
subscriber_only,
|
||||
approval_needed,
|
||||
no_subscriptions,
|
||||
custom,
|
||||
} => {
|
||||
let policy = PostPolicy {
|
||||
pk: 0,
|
||||
list: list.pk,
|
||||
announce_only,
|
||||
subscriber_only,
|
||||
approval_needed,
|
||||
no_subscriptions,
|
||||
custom,
|
||||
};
|
||||
let new_val = db.set_list_policy(policy)?;
|
||||
println!("Added new policy with pk = {}", new_val.pk());
|
||||
}
|
||||
RemovePolicy { pk } => {
|
||||
db.remove_list_policy(list.pk, pk)?;
|
||||
println!("Removed policy with pk = {}", pk);
|
||||
}
|
||||
AddListOwner { address, name } => {
|
||||
let list_owner = ListOwner {
|
||||
pk: 0,
|
||||
list: list.pk,
|
||||
address,
|
||||
name,
|
||||
};
|
||||
let new_val = db.add_list_owner(list_owner)?;
|
||||
println!("Added new list owner {}", new_val);
|
||||
}
|
||||
RemoveListOwner { pk } => {
|
||||
db.remove_list_owner(list.pk, pk)?;
|
||||
println!("Removed list owner with pk = {}", pk);
|
||||
}
|
||||
EnableMembership { address } => {
|
||||
let changeset = ListMembershipChangeset {
|
||||
list: list.pk,
|
||||
address,
|
||||
name: None,
|
||||
digest: None,
|
||||
hide_address: None,
|
||||
receive_duplicates: None,
|
||||
receive_own_posts: None,
|
||||
receive_confirmation: None,
|
||||
enabled: Some(true),
|
||||
};
|
||||
db.update_member(changeset)?;
|
||||
}
|
||||
DisableMembership { address } => {
|
||||
let changeset = ListMembershipChangeset {
|
||||
list: list.pk,
|
||||
address,
|
||||
name: None,
|
||||
digest: None,
|
||||
hide_address: None,
|
||||
receive_duplicates: None,
|
||||
receive_own_posts: None,
|
||||
receive_confirmation: None,
|
||||
enabled: Some(false),
|
||||
};
|
||||
db.update_member(changeset)?;
|
||||
}
|
||||
Update {
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
} => {
|
||||
let description = if description
|
||||
.as_ref()
|
||||
.map(|s: &String| s.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(description)
|
||||
};
|
||||
let archive_url = if archive_url
|
||||
.as_ref()
|
||||
.map(|s: &String| s.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(archive_url)
|
||||
};
|
||||
let changeset = MailingListChangeset {
|
||||
pk: list.pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
};
|
||||
db.update_list(changeset)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
CreateList {
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
} => {
|
||||
let new = db.create_list(MailingList {
|
||||
pk: 0,
|
||||
name,
|
||||
id,
|
||||
description,
|
||||
address,
|
||||
archive_url,
|
||||
})?;
|
||||
log::trace!("created new list {:#?}", new);
|
||||
if !opt.quiet {
|
||||
println!(
|
||||
"Created new list {:?} with primary key {}",
|
||||
new.id,
|
||||
new.pk()
|
||||
);
|
||||
}
|
||||
}
|
||||
Post { dry_run } => {
|
||||
if opt.debug {
|
||||
println!("post dry_run{:?}", dry_run);
|
||||
}
|
||||
|
||||
use melib::Envelope;
|
||||
use std::io::Read;
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_to_string(&mut input)?;
|
||||
match Envelope::from_bytes(input.as_bytes(), None) {
|
||||
Ok(env) => {
|
||||
if opt.debug {
|
||||
eprintln!("{:?}", &env);
|
||||
}
|
||||
db.post(&env, input.as_bytes(), dry_run)?;
|
||||
}
|
||||
Err(err) if input.trim().is_empty() => {
|
||||
eprintln!("Empty input, abort.");
|
||||
return Err(err.into());
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Could not parse message: {}", err);
|
||||
let p = db.conf().save_message(input)?;
|
||||
eprintln!("Message saved at {}", p.display());
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
ErrorQueue { cmd } => match cmd {
|
||||
ErrorQueueCommand::List => {
|
||||
let errors = db.error_queue()?;
|
||||
if errors.is_empty() {
|
||||
println!("Error queue is empty.");
|
||||
} else {
|
||||
for e in errors {
|
||||
println!(
|
||||
"- {} {} {} {} {}",
|
||||
e["pk"],
|
||||
e["datetime"],
|
||||
e["from_address"],
|
||||
e["to_address"],
|
||||
e["subject"]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ErrorQueueCommand::Print { index, json } => {
|
||||
let mut errors = db.error_queue()?;
|
||||
if !index.is_empty() {
|
||||
errors.retain(|el| index.contains(&el.pk()));
|
||||
}
|
||||
if errors.is_empty() {
|
||||
println!("Error queue is empty.");
|
||||
} else {
|
||||
for e in errors {
|
||||
if json {
|
||||
println!("{:#}", e);
|
||||
} else {
|
||||
println!("{}", e["message"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ErrorQueueCommand::Delete { index, quiet } => {
|
||||
let mut errors = db.error_queue()?;
|
||||
if !index.is_empty() {
|
||||
errors.retain(|el| index.contains(&el.pk()));
|
||||
}
|
||||
if errors.is_empty() {
|
||||
if !quiet {
|
||||
println!("Error queue is empty.");
|
||||
}
|
||||
} else {
|
||||
if !quiet {
|
||||
println!("Deleting error queue elements {:?}", &index);
|
||||
}
|
||||
db.delete_from_error_queue(index)?;
|
||||
if !quiet {
|
||||
for e in errors {
|
||||
println!("{}", e["message"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ImportMaildir {
|
||||
list_id,
|
||||
mut maildir_path,
|
||||
} => {
|
||||
let list = match list!(db, list_id) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(format!("No list with id or pk {} was found", list_id).into());
|
||||
}
|
||||
};
|
||||
use melib::backends::maildir::MaildirPathTrait;
|
||||
use melib::{Envelope, EnvelopeHash};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Read;
|
||||
|
||||
if !maildir_path.is_absolute() {
|
||||
maildir_path = std::env::current_dir()
|
||||
.expect("could not detect current directory")
|
||||
.join(&maildir_path);
|
||||
}
|
||||
|
||||
fn get_file_hash(file: &std::path::Path) -> EnvelopeHash {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
file.hash(&mut hasher);
|
||||
EnvelopeHash(hasher.finish())
|
||||
}
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
let files =
|
||||
melib::backends::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)?;
|
||||
let mut ctr = 0;
|
||||
for file in files {
|
||||
let hash = get_file_hash(&file);
|
||||
let mut reader = std::io::BufReader::new(std::fs::File::open(&file)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
if let Ok(mut env) = Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
|
||||
env.set_hash(hash);
|
||||
db.insert_post(list.pk, &buf, &env)?;
|
||||
ctr += 1;
|
||||
}
|
||||
}
|
||||
println!("Inserted {} posts to {}.", ctr, list_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> std::result::Result<(), i32> {
|
||||
let opt = Opt::from_args();
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.module("mailpot")
|
||||
.quiet(opt.quiet)
|
||||
.verbosity(opt.verbose)
|
||||
.timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off))
|
||||
.init()
|
||||
.unwrap();
|
||||
if let Err(err) = run_app(opt) {
|
||||
println!("{}", err.display_chain());
|
||||
std::process::exit(-1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "mailpot"
|
||||
version = "0.1.0"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2018"
|
||||
license = "LICENSE"
|
||||
readme = "README.md"
|
||||
description = "mailing list manager"
|
||||
repository = "https://github.com/meli/mailpot"
|
||||
keywords = ["mail", "mailing-lists"]
|
||||
categories = ["email"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.58"
|
||||
chrono = { version = "^0.4", features = ["serde", ] }
|
||||
error-chain = { version = "0.12.4", default-features = false }
|
||||
log = "0.4"
|
||||
melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms", "maildir_backend"], git = "https://github.com/meli/meli", rev = "2447a2c" }
|
||||
rusqlite = { version = "^0.28", features = ["bundled", "trace", "hooks"] }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
||||
serde_json = "^1"
|
||||
toml = "^0.5"
|
||||
xdg = "2.4.1"
|
||||
|
||||
[dev-dependencies]
|
||||
mailin-embedded = { version = "0.7", features = ["rtls"] }
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "blocking"] }
|
||||
stderrlog = "^0.5"
|
||||
tempfile = "3.3.0"
|
|
@ -17,53 +17,17 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
// // Source: https://stackoverflow.com/a/64535181
|
||||
// fn is_output_file_outdated<P1, P2>(input: P1, output: P2) -> io::Result<bool>
|
||||
// where
|
||||
// P1: AsRef<Path>,
|
||||
// P2: AsRef<Path>,
|
||||
// {
|
||||
// let out_meta = metadata(output);
|
||||
// if let Ok(meta) = out_meta {
|
||||
// let output_mtime = meta.modified()?;
|
||||
//
|
||||
// // if input file is more recent than our output, we are outdated
|
||||
// let input_meta = metadata(input)?;
|
||||
// let input_mtime = input_meta.modified()?;
|
||||
//
|
||||
// Ok(input_mtime > output_mtime)
|
||||
// } else {
|
||||
// // output file not found, we are outdated
|
||||
// Ok(true)
|
||||
// }
|
||||
// }
|
||||
|
||||
include!("make_migrations.rs");
|
||||
|
||||
const MIGRATION_RS: &str = "src/migrations.rs.inc";
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=src/migrations.rs.inc");
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
println!("cargo:rerun-if-changed=src/schema.sql.m4");
|
||||
|
||||
let mut output = Command::new("m4")
|
||||
let output = Command::new("m4")
|
||||
.arg("./src/schema.sql.m4")
|
||||
.output()
|
||||
.unwrap();
|
||||
if String::from_utf8_lossy(&output.stdout).trim().is_empty() {
|
||||
panic!(
|
||||
"m4 output is empty. stderr was {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let user_version: i32 = make_migrations("migrations", MIGRATION_RS, &mut output.stdout);
|
||||
let mut verify = Command::new(std::env::var("SQLITE_BIN").unwrap_or("sqlite3".into()))
|
||||
let mut verify = Command::new("sqlite3")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
|
@ -87,9 +51,4 @@ fn main() {
|
|||
}
|
||||
let mut file = std::fs::File::create("./src/schema.sql").unwrap();
|
||||
file.write_all(&output.stdout).unwrap();
|
||||
file.write_all(
|
||||
&format!("\n\n-- Set current schema version.\n\nPRAGMA user_version = {user_version};\n")
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
|
@ -17,15 +17,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use chrono::prelude::*;
|
||||
|
||||
use super::errors::*;
|
||||
use chrono::prelude::*;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// How to send e-mail.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
@ -33,8 +29,7 @@ use super::errors::*;
|
|||
pub enum SendMail {
|
||||
/// A `melib` configuration for talking to an SMTP server.
|
||||
Smtp(melib::smtp::SmtpServerConf),
|
||||
/// A plain shell command passed to `sh -c` with the e-mail passed in the
|
||||
/// stdin.
|
||||
/// A plain shell command passed to `sh -c` with the e-mail passed in the stdin.
|
||||
ShellCommand(String),
|
||||
}
|
||||
|
||||
|
@ -47,27 +42,21 @@ pub struct Configuration {
|
|||
pub db_path: PathBuf,
|
||||
/// The directory where data are stored.
|
||||
pub data_path: PathBuf,
|
||||
/// Instance administrators (List of e-mail addresses). Optional.
|
||||
#[serde(default)]
|
||||
pub administrators: Vec<String>,
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
/// Create a new configuration value from a given database path value.
|
||||
///
|
||||
/// If you wish to create a new database with this configuration, use
|
||||
/// [`Connection::open_or_create_db`](crate::Connection::open_or_create_db).
|
||||
/// To open an existing database, use
|
||||
/// [`Database::open_db`](crate::Connection::open_db).
|
||||
/// If you wish to create a new database with this configuration, use [`Connection::open_or_create_db`](crate::Connection::open_or_create_db).
|
||||
/// To open an existing database, use [`Database::open_db`](crate::Connection::open_db).
|
||||
pub fn new(db_path: impl Into<PathBuf>) -> Self {
|
||||
let db_path = db_path.into();
|
||||
Self {
|
||||
Configuration {
|
||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
data_path: db_path
|
||||
.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or_else(|| db_path.clone()),
|
||||
administrators: vec![],
|
||||
db_path,
|
||||
}
|
||||
}
|
||||
|
@ -76,18 +65,12 @@ impl Configuration {
|
|||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let mut s = String::new();
|
||||
let mut file = std::fs::File::open(path)
|
||||
.with_context(|| format!("Configuration file {} not found.", path.display()))?;
|
||||
file.read_to_string(&mut s)
|
||||
.with_context(|| format!("Could not read from file {}.", path.display()))?;
|
||||
let config: Self = toml::from_str(&s)
|
||||
.map_err(anyhow::Error::from)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not parse configuration file `{}` successfully: ",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
file.read_to_string(&mut s)?;
|
||||
let config: Configuration = toml::from_str(&s).context(format!(
|
||||
"Could not parse configuration file `{}` succesfully: ",
|
||||
path.display()
|
||||
))?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
@ -110,20 +93,14 @@ impl Configuration {
|
|||
}
|
||||
|
||||
debug_assert!(path != self.db_path());
|
||||
let mut file = std::fs::File::create(&path)
|
||||
.with_context(|| format!("Could not create file {}.", path.display()))?;
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.with_context(|| format!("Could not fstat file {}.", path.display()))?;
|
||||
let mut file = std::fs::File::create(&path)?;
|
||||
let metadata = file.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
file.set_permissions(permissions)
|
||||
.with_context(|| format!("Could not chmod 600 file {}.", path.display()))?;
|
||||
file.write_all(msg.as_bytes())
|
||||
.with_context(|| format!("Could not write message to file {}.", path.display()))?;
|
||||
file.flush()
|
||||
.with_context(|| format!("Could not flush message I/O to file {}.", path.display()))?;
|
||||
file.set_permissions(permissions)?;
|
||||
file.write_all(msg.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
|
@ -139,29 +116,3 @@ impl Configuration {
|
|||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_parse_error() {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let conf_path = tmp_dir.path().join("conf.toml");
|
||||
std::fs::write(&conf_path, b"afjsad skas as a as\n\n\n\n\t\x11\n").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Configuration::from_file(&conf_path)
|
||||
.unwrap_err()
|
||||
.display_chain()
|
||||
.to_string(),
|
||||
format!(
|
||||
"[1] Could not parse configuration file `{}` successfully: Caused by:\n[2] \
|
||||
Error: expected an equals, found an identifier at line 1 column 8\n",
|
||||
conf_path.display()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,659 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Mailpot database and methods.
|
||||
|
||||
use super::Configuration;
|
||||
use super::*;
|
||||
use crate::ErrorKind::*;
|
||||
use melib::Envelope;
|
||||
use models::changesets::*;
|
||||
use rusqlite::Connection as DbConnection;
|
||||
use rusqlite::OptionalExtension;
|
||||
use std::convert::TryFrom;
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// A connection to a `mailpot` database.
|
||||
pub struct Connection {
|
||||
/// The `rusqlite` connection handle.
|
||||
pub connection: DbConnection,
|
||||
conf: Configuration,
|
||||
}
|
||||
|
||||
mod error_queue;
|
||||
pub use error_queue::*;
|
||||
mod posts;
|
||||
pub use posts::*;
|
||||
mod members;
|
||||
pub use members::*;
|
||||
|
||||
fn log_callback(error_code: std::ffi::c_int, message: &str) {
|
||||
match error_code {
|
||||
rusqlite::ffi::SQLITE_NOTICE => log::info!("{}", message),
|
||||
rusqlite::ffi::SQLITE_WARNING => log::warn!("{}", message),
|
||||
_ => log::error!("{error_code} {}", message),
|
||||
}
|
||||
}
|
||||
|
||||
fn user_authorizer_callback(
|
||||
auth_context: rusqlite::hooks::AuthContext<'_>,
|
||||
) -> rusqlite::hooks::Authorization {
|
||||
use rusqlite::hooks::{AuthAction, Authorization};
|
||||
|
||||
// [ref:sync_auth_doc] sync with `untrusted()` rustdoc when changing this.
|
||||
match auth_context.action {
|
||||
AuthAction::Delete {
|
||||
table_name: "error_queue" | "queue" | "candidate_membership" | "membership",
|
||||
}
|
||||
| AuthAction::Insert {
|
||||
table_name: "post" | "error_queue" | "queue" | "candidate_membership" | "membership",
|
||||
}
|
||||
| AuthAction::Select
|
||||
| AuthAction::Savepoint { .. }
|
||||
| AuthAction::Transaction { .. }
|
||||
| AuthAction::Read { .. }
|
||||
| AuthAction::Function {
|
||||
function_name: "strftime",
|
||||
} => Authorization::Allow,
|
||||
_ => Authorization::Deny,
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// Creates a new database connection.
|
||||
///
|
||||
/// `Connection` supports a limited subset of operations by default (see
|
||||
/// [`Connection::untrusted`]).
|
||||
/// Use [`Connection::trusted`] to remove these limits.
|
||||
pub fn open_db(conf: Configuration) -> Result<Self> {
|
||||
use rusqlite::config::DbConfig;
|
||||
use std::sync::Once;
|
||||
|
||||
static INIT_SQLITE_LOGGING: Once = Once::new();
|
||||
|
||||
if !conf.db_path.exists() {
|
||||
return Err("Database doesn't exist".into());
|
||||
}
|
||||
INIT_SQLITE_LOGGING.call_once(|| {
|
||||
unsafe { rusqlite::trace::config_log(Some(log_callback)).unwrap() };
|
||||
});
|
||||
let conn = DbConnection::open(conf.db_path.to_str().unwrap())?;
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_FKEY, true)?;
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)?;
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?;
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?;
|
||||
conn.busy_timeout(core::time::Duration::from_millis(500))?;
|
||||
conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?;
|
||||
conn.authorizer(Some(user_authorizer_callback));
|
||||
Ok(Connection {
|
||||
conf,
|
||||
connection: conn,
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes operational limits from this connection. (see [`Connection::untrusted`])
|
||||
#[must_use]
|
||||
pub fn trusted(self) -> Self {
|
||||
self.connection
|
||||
.authorizer::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>(
|
||||
None,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
// [tag:sync_auth_doc]
|
||||
/// Sets operational limits for this connection.
|
||||
///
|
||||
/// - Allow `INSERT`, `DELETE` only for "error_queue", "queue", "candidate_membership", "membership".
|
||||
/// - Allow `INSERT` only for "post".
|
||||
/// - Allow read access to all tables.
|
||||
/// - Allow `SELECT`, `TRANSACTION`, `SAVEPOINT`, and the `strftime` function.
|
||||
/// - Deny everything else.
|
||||
pub fn untrusted(self) -> Self {
|
||||
self.connection.authorizer(Some(user_authorizer_callback));
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a database if it doesn't exist and then open it.
|
||||
pub fn open_or_create_db(conf: Configuration) -> Result<Self> {
|
||||
if !conf.db_path.exists() {
|
||||
let db_path = &conf.db_path;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
info!("Creating database in {}", db_path.display());
|
||||
std::fs::File::create(db_path).context("Could not create db path")?;
|
||||
|
||||
let mut child = Command::new("sqlite3")
|
||||
.arg(db_path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
std::thread::spawn(move || {
|
||||
stdin
|
||||
.write_all(include_bytes!("./schema.sql"))
|
||||
.expect("failed to write to stdin");
|
||||
stdin.flush().expect("could not flush stdin");
|
||||
});
|
||||
let output = child.wait_with_output()?;
|
||||
if !output.status.success() {
|
||||
return Err(format!("Could not initialize sqlite3 database at {}: sqlite3 returned exit code {} and stderr {} {}", db_path.display(), output.status.code().unwrap_or_default(), String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout)).into());
|
||||
}
|
||||
|
||||
let file = std::fs::File::open(db_path)?;
|
||||
let metadata = file.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
file.set_permissions(permissions)?;
|
||||
}
|
||||
Self::open_db(conf)
|
||||
}
|
||||
|
||||
/// Returns a connection's configuration.
|
||||
pub fn conf(&self) -> &Configuration {
|
||||
&self.conf
|
||||
}
|
||||
|
||||
/// Loads archive databases from [`Configuration::data_path`], if any.
|
||||
pub fn load_archives(&self) -> Result<()> {
|
||||
let mut stmt = self.connection.prepare("ATTACH ? AS ?;")?;
|
||||
for archive in std::fs::read_dir(&self.conf.data_path)? {
|
||||
let archive = archive?;
|
||||
let path = archive.path();
|
||||
let name = path.file_name().unwrap_or_default();
|
||||
if path == self.conf.db_path {
|
||||
continue;
|
||||
}
|
||||
stmt.execute(rusqlite::params![
|
||||
path.to_str().unwrap(),
|
||||
name.to_str().unwrap()
|
||||
])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a vector of existing mailing lists.
|
||||
pub fn lists(&self) -> Result<Vec<DbVal<MailingList>>> {
|
||||
let mut stmt = self.connection.prepare("SELECT * FROM mailing_lists;")?;
|
||||
let list_iter = stmt.query_map([], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
MailingList {
|
||||
pk,
|
||||
name: row.get("name")?,
|
||||
id: row.get("id")?,
|
||||
address: row.get("address")?,
|
||||
description: row.get("description")?,
|
||||
archive_url: row.get("archive_url")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut ret = vec![];
|
||||
for list in list_iter {
|
||||
let list = list?;
|
||||
ret.push(list);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Fetch a mailing list by primary key.
|
||||
pub fn list(&self, pk: i64) -> Result<DbVal<MailingList>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM mailing_lists WHERE pk = ?;")?;
|
||||
let ret = stmt
|
||||
.query_row([&pk], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
MailingList {
|
||||
pk,
|
||||
name: row.get("name")?,
|
||||
id: row.get("id")?,
|
||||
address: row.get("address")?,
|
||||
description: row.get("description")?,
|
||||
archive_url: row.get("archive_url")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})
|
||||
.optional()?;
|
||||
if let Some(ret) = ret {
|
||||
Ok(ret)
|
||||
} else {
|
||||
Err(Error::from(NotFound("list or list policy not found!")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a mailing list by id.
|
||||
pub fn list_by_id<S: AsRef<str>>(&self, id: S) -> Result<Option<DbVal<MailingList>>> {
|
||||
let id = id.as_ref();
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM mailing_lists WHERE id = ?;")?;
|
||||
let ret = stmt
|
||||
.query_row([&id], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
MailingList {
|
||||
pk,
|
||||
name: row.get("name")?,
|
||||
id: row.get("id")?,
|
||||
address: row.get("address")?,
|
||||
description: row.get("description")?,
|
||||
archive_url: row.get("archive_url")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})
|
||||
.optional()?;
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Create a new list.
|
||||
pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("INSERT INTO mailing_lists(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) RETURNING *;")?;
|
||||
let ret = stmt.query_row(
|
||||
rusqlite::params![
|
||||
&new_val.name,
|
||||
&new_val.id,
|
||||
&new_val.address,
|
||||
new_val.description.as_ref(),
|
||||
new_val.archive_url.as_ref(),
|
||||
],
|
||||
|row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
MailingList {
|
||||
pk,
|
||||
name: row.get("name")?,
|
||||
id: row.get("id")?,
|
||||
address: row.get("address")?,
|
||||
description: row.get("description")?,
|
||||
archive_url: row.get("archive_url")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
|
||||
trace!("create_list {:?}.", &ret);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Remove an existing list policy.
|
||||
///
|
||||
/// ```
|
||||
/// # use mailpot::{models::*, Configuration, Connection, SendMail};
|
||||
/// # use tempfile::TempDir;
|
||||
///
|
||||
/// # let tmp_dir = TempDir::new().unwrap();
|
||||
/// # let db_path = tmp_dir.path().join("mpot.db");
|
||||
/// # let config = Configuration {
|
||||
/// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
/// # db_path: db_path.clone(),
|
||||
/// # data_path: tmp_dir.path().to_path_buf(),
|
||||
/// # };
|
||||
///
|
||||
/// # fn do_test(config: Configuration) {
|
||||
/// let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
/// let list_pk = db.create_list(MailingList {
|
||||
/// pk: 0,
|
||||
/// name: "foobar chat".into(),
|
||||
/// id: "foo-chat".into(),
|
||||
/// address: "foo-chat@example.com".into(),
|
||||
/// description: None,
|
||||
/// archive_url: None,
|
||||
/// }).unwrap().pk;
|
||||
/// db.set_list_policy(
|
||||
/// PostPolicy {
|
||||
/// pk: 0,
|
||||
/// list: list_pk,
|
||||
/// announce_only: false,
|
||||
/// subscriber_only: true,
|
||||
/// approval_needed: false,
|
||||
/// no_subscriptions: false,
|
||||
/// custom: false,
|
||||
/// },
|
||||
/// ).unwrap();
|
||||
/// db.remove_list_policy(1, 1).unwrap();
|
||||
/// # }
|
||||
/// # do_test(config);
|
||||
/// ```
|
||||
pub fn remove_list_policy(&self, list_pk: i64, policy_pk: i64) -> Result<()> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("DELETE FROM post_policy WHERE pk = ? AND list = ? RETURNING *;")?;
|
||||
stmt.query_row(rusqlite::params![&policy_pk, &list_pk,], |_| Ok(()))
|
||||
.map_err(|err| {
|
||||
if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
|
||||
Error::from(err).chain_err(|| NotFound("list or list policy not found!"))
|
||||
} else {
|
||||
err.into()
|
||||
}
|
||||
})?;
|
||||
|
||||
trace!("remove_list_policy {} {}.", list_pk, policy_pk);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ```should_panic
|
||||
/// # use mailpot::{models::*, Configuration, Connection, SendMail};
|
||||
/// # use tempfile::TempDir;
|
||||
///
|
||||
/// # let tmp_dir = TempDir::new().unwrap();
|
||||
/// # let db_path = tmp_dir.path().join("mpot.db");
|
||||
/// # let config = Configuration {
|
||||
/// # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
/// # db_path: db_path.clone(),
|
||||
/// # data_path: tmp_dir.path().to_path_buf(),
|
||||
/// # };
|
||||
///
|
||||
/// # fn do_test(config: Configuration) {
|
||||
/// let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
/// db.remove_list_policy(1, 1).unwrap();
|
||||
/// # }
|
||||
/// # do_test(config);
|
||||
/// ```
|
||||
#[cfg(doc)]
|
||||
pub fn remove_list_policy_panic() {}
|
||||
|
||||
/// Set the unique post policy for a list.
|
||||
pub fn set_list_policy(&self, policy: PostPolicy) -> Result<DbVal<PostPolicy>> {
|
||||
if !(policy.announce_only
|
||||
|| policy.subscriber_only
|
||||
|| policy.approval_needed
|
||||
|| policy.no_subscriptions
|
||||
|| policy.custom)
|
||||
{
|
||||
return Err(
|
||||
"Cannot add empty policy. Having no policies is probably what you want to do."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let list_pk = policy.list;
|
||||
|
||||
let mut stmt = self.connection.prepare("INSERT OR REPLACE INTO post_policy(list, announce_only, subscriber_only, approval_needed, no_subscriptions, custom) VALUES (?, ?, ?, ?, ?, ?) RETURNING *;")?;
|
||||
let ret = stmt
|
||||
.query_row(
|
||||
rusqlite::params![
|
||||
&list_pk,
|
||||
&policy.announce_only,
|
||||
&policy.subscriber_only,
|
||||
&policy.approval_needed,
|
||||
&policy.no_subscriptions,
|
||||
&policy.custom,
|
||||
],
|
||||
|row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
PostPolicy {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
announce_only: row.get("announce_only")?,
|
||||
subscriber_only: row.get("subscriber_only")?,
|
||||
approval_needed: row.get("approval_needed")?,
|
||||
no_subscriptions: row.get("no_subscriptions")?,
|
||||
custom: row.get("custom")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
},
|
||||
)
|
||||
.map_err(|err| {
|
||||
if matches!(
|
||||
err,
|
||||
rusqlite::Error::SqliteFailure(
|
||||
rusqlite::ffi::Error {
|
||||
code: rusqlite::ffi::ErrorCode::ConstraintViolation,
|
||||
extended_code: 787
|
||||
},
|
||||
_
|
||||
)
|
||||
) {
|
||||
Error::from(err).chain_err(|| NotFound("Could not find a list with this pk."))
|
||||
} else {
|
||||
err.into()
|
||||
}
|
||||
})?;
|
||||
|
||||
trace!("set_list_policy {:?}.", &ret);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Fetch all posts of a mailing list.
|
||||
pub fn list_posts(
|
||||
&self,
|
||||
list_pk: i64,
|
||||
_date_range: Option<(String, String)>,
|
||||
) -> Result<Vec<DbVal<Post>>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT pk, list, address, message_id, message, timestamp, datetime, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') as month_year FROM post WHERE list = ?;")?;
|
||||
let iter = stmt.query_map(rusqlite::params![&list_pk,], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
Post {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
message_id: row.get("message_id")?,
|
||||
message: row.get("message")?,
|
||||
timestamp: row.get("timestamp")?,
|
||||
datetime: row.get("datetime")?,
|
||||
month_year: row.get("month_year")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
let mut ret = vec![];
|
||||
for post in iter {
|
||||
let post = post?;
|
||||
ret.push(post);
|
||||
}
|
||||
|
||||
trace!("list_posts {:?}.", &ret);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Fetch the post policy of a mailing list.
|
||||
pub fn list_policy(&self, pk: i64) -> Result<Option<DbVal<PostPolicy>>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM post_policy WHERE list = ?;")?;
|
||||
let ret = stmt
|
||||
.query_row([&pk], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
PostPolicy {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
announce_only: row.get("announce_only")?,
|
||||
subscriber_only: row.get("subscriber_only")?,
|
||||
approval_needed: row.get("approval_needed")?,
|
||||
no_subscriptions: row.get("no_subscriptions")?,
|
||||
custom: row.get("custom")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})
|
||||
.optional()?;
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Fetch the owners of a mailing list.
|
||||
pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM list_owner WHERE list = ?;")?;
|
||||
let list_iter = stmt.query_map([&pk], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListOwner {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
name: row.get("name")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut ret = vec![];
|
||||
for list in list_iter {
|
||||
let list = list?;
|
||||
ret.push(list);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Remove an owner of a mailing list.
|
||||
pub fn remove_list_owner(&self, list_pk: i64, owner_pk: i64) -> Result<()> {
|
||||
self.connection
|
||||
.query_row(
|
||||
"DELETE FROM list_owner WHERE list = ? AND pk = ? RETURNING *;",
|
||||
rusqlite::params![&list_pk, &owner_pk],
|
||||
|_| Ok(()),
|
||||
)
|
||||
.map_err(|err| {
|
||||
if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
|
||||
Error::from(err).chain_err(|| NotFound("list or list owner not found!"))
|
||||
} else {
|
||||
err.into()
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add an owner of a mailing list.
|
||||
pub fn add_list_owner(&self, list_owner: ListOwner) -> Result<DbVal<ListOwner>> {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"INSERT OR REPLACE INTO list_owner(list, address, name) VALUES (?, ?, ?) RETURNING *;",
|
||||
)?;
|
||||
let list_pk = list_owner.list;
|
||||
let ret = stmt
|
||||
.query_row(
|
||||
rusqlite::params![&list_pk, &list_owner.address, &list_owner.name,],
|
||||
|row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListOwner {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
name: row.get("name")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
},
|
||||
)
|
||||
.map_err(|err| {
|
||||
if matches!(
|
||||
err,
|
||||
rusqlite::Error::SqliteFailure(
|
||||
rusqlite::ffi::Error {
|
||||
code: rusqlite::ffi::ErrorCode::ConstraintViolation,
|
||||
extended_code: 787
|
||||
},
|
||||
_
|
||||
)
|
||||
) {
|
||||
Error::from(err).chain_err(|| NotFound("Could not find a list with this pk."))
|
||||
} else {
|
||||
err.into()
|
||||
}
|
||||
})?;
|
||||
|
||||
trace!("add_list_owner {:?}.", &ret);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Update a mailing list.
|
||||
pub fn update_list(&mut self, change_set: MailingListChangeset) -> Result<()> {
|
||||
if matches!(
|
||||
change_set,
|
||||
MailingListChangeset {
|
||||
pk: _,
|
||||
name: None,
|
||||
id: None,
|
||||
address: None,
|
||||
description: None,
|
||||
archive_url: None
|
||||
}
|
||||
) {
|
||||
return self.list(change_set.pk).map(|_| ());
|
||||
}
|
||||
|
||||
let MailingListChangeset {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
} = change_set;
|
||||
let tx = self.connection.transaction()?;
|
||||
|
||||
macro_rules! update {
|
||||
($field:tt) => {{
|
||||
if let Some($field) = $field {
|
||||
tx.execute(
|
||||
concat!(
|
||||
"UPDATE mailing_lists SET ",
|
||||
stringify!($field),
|
||||
" = ? WHERE pk = ?;"
|
||||
),
|
||||
rusqlite::params![&$field, &pk],
|
||||
)?;
|
||||
}
|
||||
}};
|
||||
}
|
||||
update!(name);
|
||||
update!(id);
|
||||
update!(address);
|
||||
update!(description);
|
||||
update!(archive_url);
|
||||
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the post filters of a mailing list.
|
||||
pub fn list_filters(
|
||||
&self,
|
||||
_list: &DbVal<MailingList>,
|
||||
) -> Vec<Box<dyn crate::mail::message_filters::PostFilter>> {
|
||||
use crate::mail::message_filters::*;
|
||||
vec![
|
||||
Box::new(FixCRLF),
|
||||
Box::new(PostRightsCheck),
|
||||
Box::new(AddListHeaders),
|
||||
Box::new(FinalizeRecipients),
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
impl Connection {
|
||||
/// Insert a received email into the error queue.
|
||||
pub fn insert_to_error_queue(&self, env: &Envelope, raw: &[u8], reason: String) -> Result<i64> {
|
||||
let mut stmt = self.connection.prepare("INSERT INTO error_queue(error, to_address, from_address, subject, message_id, message, timestamp, datetime) VALUES(?, ?, ?, ?, ?, ?, ?, ?) RETURNING pk;")?;
|
||||
let pk = stmt.query_row(
|
||||
rusqlite::params![
|
||||
&reason,
|
||||
&env.field_to_to_string(),
|
||||
&env.field_from_to_string(),
|
||||
&env.subject(),
|
||||
&env.message_id().to_string(),
|
||||
raw,
|
||||
&env.timestamp,
|
||||
&env.date,
|
||||
],
|
||||
|row| {
|
||||
let pk: i64 = row.get("pk")?;
|
||||
Ok(pk)
|
||||
},
|
||||
)?;
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
/// Fetch all error queue entries.
|
||||
pub fn error_queue(&self) -> Result<Vec<DbVal<Value>>> {
|
||||
let mut stmt = self.connection.prepare("SELECT * FROM error_queue;")?;
|
||||
let error_iter = stmt.query_map([], |row| {
|
||||
let pk = row.get::<_, i64>("pk")?;
|
||||
Ok(DbVal(
|
||||
json!({
|
||||
"pk" : pk,
|
||||
"error": row.get::<_, String>("error")?,
|
||||
"to_address": row.get::<_, String>("to_address")?,
|
||||
"from_address": row.get::<_, String>("from_address")?,
|
||||
"subject": row.get::<_, String>("subject")?,
|
||||
"message_id": row.get::<_, String>("message_id")?,
|
||||
"message": row.get::<_, Vec<u8>>("message")?,
|
||||
"timestamp": row.get::<_, u64>("timestamp")?,
|
||||
"datetime": row.get::<_, String>("datetime")?,
|
||||
}),
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut ret = vec![];
|
||||
for error in error_iter {
|
||||
let error = error?;
|
||||
ret.push(error);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Delete error queue entries.
|
||||
pub fn delete_from_error_queue(&mut self, index: Vec<i64>) -> Result<()> {
|
||||
let tx = self.connection.transaction()?;
|
||||
|
||||
if index.is_empty() {
|
||||
tx.execute("DELETE FROM error_queue;", [])?;
|
||||
} else {
|
||||
for i in index {
|
||||
tx.execute(
|
||||
"DELETE FROM error_queue WHERE pk = ?;",
|
||||
rusqlite::params![i],
|
||||
)?;
|
||||
}
|
||||
};
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Connection {
|
||||
/// Fetch all members of a mailing list.
|
||||
pub fn list_members(&self, pk: i64) -> Result<Vec<DbVal<ListMembership>>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM membership WHERE list = ?;")?;
|
||||
let list_iter = stmt.query_map([&pk], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListMembership {
|
||||
pk: row.get("pk")?,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
name: row.get("name")?,
|
||||
digest: row.get("digest")?,
|
||||
hide_address: row.get("hide_address")?,
|
||||
receive_duplicates: row.get("receive_duplicates")?,
|
||||
receive_own_posts: row.get("receive_own_posts")?,
|
||||
receive_confirmation: row.get("receive_confirmation")?,
|
||||
enabled: row.get("enabled")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut ret = vec![];
|
||||
for list in list_iter {
|
||||
let list = list?;
|
||||
ret.push(list);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Fetch mailing list member.
|
||||
pub fn list_member(&self, list_pk: i64, pk: i64) -> Result<DbVal<ListMembership>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM membership WHERE list = ? AND pk = ?;")?;
|
||||
|
||||
let ret = stmt.query_row([&list_pk, &pk], |row| {
|
||||
let _pk: i64 = row.get("pk")?;
|
||||
debug_assert_eq!(pk, _pk);
|
||||
Ok(DbVal(
|
||||
ListMembership {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
name: row.get("name")?,
|
||||
digest: row.get("digest")?,
|
||||
hide_address: row.get("hide_address")?,
|
||||
receive_duplicates: row.get("receive_duplicates")?,
|
||||
receive_own_posts: row.get("receive_own_posts")?,
|
||||
receive_confirmation: row.get("receive_confirmation")?,
|
||||
enabled: row.get("enabled")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Fetch mailing list member by their address.
|
||||
pub fn list_member_by_address(
|
||||
&self,
|
||||
list_pk: i64,
|
||||
address: &str,
|
||||
) -> Result<DbVal<ListMembership>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT * FROM membership WHERE list = ? AND address = ?;")?;
|
||||
|
||||
let ret = stmt.query_row(rusqlite::params![&list_pk, &address], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
let address_ = row.get("address")?;
|
||||
debug_assert_eq!(address, &address_);
|
||||
Ok(DbVal(
|
||||
ListMembership {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: address_,
|
||||
name: row.get("name")?,
|
||||
digest: row.get("digest")?,
|
||||
hide_address: row.get("hide_address")?,
|
||||
receive_duplicates: row.get("receive_duplicates")?,
|
||||
receive_own_posts: row.get("receive_own_posts")?,
|
||||
receive_confirmation: row.get("receive_confirmation")?,
|
||||
enabled: row.get("enabled")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Add member to mailing list.
|
||||
pub fn add_member(
|
||||
&self,
|
||||
list_pk: i64,
|
||||
mut new_val: ListMembership,
|
||||
) -> Result<DbVal<ListMembership>> {
|
||||
new_val.list = list_pk;
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("INSERT INTO membership(list, address, name, enabled, digest, hide_address, receive_duplicates, receive_own_posts, receive_confirmation) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *;")?;
|
||||
let ret = stmt.query_row(
|
||||
rusqlite::params![
|
||||
&new_val.list,
|
||||
&new_val.address,
|
||||
&new_val.name,
|
||||
&new_val.enabled,
|
||||
&new_val.digest,
|
||||
&new_val.hide_address,
|
||||
&new_val.receive_duplicates,
|
||||
&new_val.receive_own_posts,
|
||||
&new_val.receive_confirmation
|
||||
],
|
||||
|row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListMembership {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
name: row.get("name")?,
|
||||
digest: row.get("digest")?,
|
||||
hide_address: row.get("hide_address")?,
|
||||
receive_duplicates: row.get("receive_duplicates")?,
|
||||
receive_own_posts: row.get("receive_own_posts")?,
|
||||
receive_confirmation: row.get("receive_confirmation")?,
|
||||
enabled: row.get("enabled")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
|
||||
trace!("add_member {:?}.", &ret);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Create membership candidate.
|
||||
pub fn add_candidate_member(&self, list_pk: i64, mut new_val: ListMembership) -> Result<i64> {
|
||||
new_val.list = list_pk;
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("INSERT INTO candidate_membership(list, address, name, accepted) VALUES(?, ?, ?, ?) RETURNING pk;")?;
|
||||
let ret = stmt.query_row(
|
||||
rusqlite::params![&new_val.list, &new_val.address, &new_val.name, None::<i64>,],
|
||||
|row| {
|
||||
let pk: i64 = row.get("pk")?;
|
||||
Ok(pk)
|
||||
},
|
||||
)?;
|
||||
|
||||
trace!("add_candidate_member {:?}.", &ret);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Accept membership candidate.
|
||||
pub fn accept_candidate_member(&mut self, pk: i64) -> Result<DbVal<ListMembership>> {
|
||||
let tx = self.connection.transaction()?;
|
||||
let mut stmt = tx
|
||||
.prepare("INSERT INTO membership(list, address, name, enabled, digest, hide_address, receive_duplicates, receive_own_posts, receive_confirmation) FROM (SELECT list, address, name FROM candidate_membership WHERE pk = ?) RETURNING *;")?;
|
||||
let ret = stmt.query_row(rusqlite::params![&pk], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListMembership {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
name: row.get("name")?,
|
||||
digest: row.get("digest")?,
|
||||
hide_address: row.get("hide_address")?,
|
||||
receive_duplicates: row.get("receive_duplicates")?,
|
||||
receive_own_posts: row.get("receive_own_posts")?,
|
||||
receive_confirmation: row.get("receive_confirmation")?,
|
||||
enabled: row.get("enabled")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
drop(stmt);
|
||||
tx.execute(
|
||||
"UPDATE candidate_membership SET accepted = ? WHERE pk = ?;",
|
||||
[&ret.pk, &pk],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
|
||||
trace!("accept_candidate_member {:?}.", &ret);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Remove a member by their address.
|
||||
pub fn remove_membership(&self, list_pk: i64, address: &str) -> Result<()> {
|
||||
self.connection
|
||||
.query_row(
|
||||
"DELETE FROM membership WHERE list_pk = ? AND address = ? RETURNING *;",
|
||||
rusqlite::params![&list_pk, &address],
|
||||
|_| Ok(()),
|
||||
)
|
||||
.map_err(|err| {
|
||||
if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
|
||||
Error::from(err).chain_err(|| NotFound("list or list owner not found!"))
|
||||
} else {
|
||||
err.into()
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update a mailing list membership.
|
||||
pub fn update_member(&mut self, change_set: ListMembershipChangeset) -> Result<()> {
|
||||
let pk = self
|
||||
.list_member_by_address(change_set.list, &change_set.address)?
|
||||
.pk;
|
||||
if matches!(
|
||||
change_set,
|
||||
ListMembershipChangeset {
|
||||
list: _,
|
||||
address: _,
|
||||
name: None,
|
||||
digest: None,
|
||||
hide_address: None,
|
||||
receive_duplicates: None,
|
||||
receive_own_posts: None,
|
||||
receive_confirmation: None,
|
||||
enabled: None,
|
||||
}
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ListMembershipChangeset {
|
||||
list,
|
||||
address: _,
|
||||
name,
|
||||
digest,
|
||||
hide_address,
|
||||
receive_duplicates,
|
||||
receive_own_posts,
|
||||
receive_confirmation,
|
||||
enabled,
|
||||
} = change_set;
|
||||
let tx = self.connection.transaction()?;
|
||||
|
||||
macro_rules! update {
|
||||
($field:tt) => {{
|
||||
if let Some($field) = $field {
|
||||
tx.execute(
|
||||
concat!(
|
||||
"UPDATE membership SET ",
|
||||
stringify!($field),
|
||||
" = ? WHERE list = ? AND pk = ?;"
|
||||
),
|
||||
rusqlite::params![&$field, &list, &pk],
|
||||
)?;
|
||||
}
|
||||
}};
|
||||
}
|
||||
update!(name);
|
||||
update!(digest);
|
||||
update!(hide_address);
|
||||
update!(receive_duplicates);
|
||||
update!(receive_own_posts);
|
||||
update!(receive_confirmation);
|
||||
update!(enabled);
|
||||
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::mail::ListRequest;
|
||||
|
||||
impl Connection {
|
||||
/// Insert a mailing list post into the database.
|
||||
pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result<i64> {
|
||||
let from_ = env.from();
|
||||
let address = if from_.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
from_[0].get_email()
|
||||
};
|
||||
let mut datetime: std::borrow::Cow<'_, str> = env.date.as_str().into();
|
||||
if env.timestamp != 0 {
|
||||
datetime = melib::datetime::timestamp_to_string(
|
||||
env.timestamp,
|
||||
Some(melib::datetime::RFC3339_FMT_WITH_TIME),
|
||||
true,
|
||||
)
|
||||
.into();
|
||||
}
|
||||
let message_id = env.message_id_display();
|
||||
let mut stmt = self.connection.prepare(
|
||||
"INSERT OR REPLACE INTO post(list, address, message_id, message, datetime, timestamp) VALUES(?, ?, ?, ?, ?, ?) RETURNING pk;",
|
||||
)?;
|
||||
let pk = stmt.query_row(
|
||||
rusqlite::params![
|
||||
&list_pk,
|
||||
&address,
|
||||
&message_id,
|
||||
&message,
|
||||
&datetime,
|
||||
&env.timestamp
|
||||
],
|
||||
|row| {
|
||||
let pk: i64 = row.get("pk")?;
|
||||
Ok(pk)
|
||||
},
|
||||
)?;
|
||||
|
||||
trace!(
|
||||
"insert_post list_pk {}, from {:?} message_id {:?} post_pk {}.",
|
||||
list_pk,
|
||||
address,
|
||||
message_id,
|
||||
pk
|
||||
);
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
/// Process a new mailing list post.
|
||||
pub fn post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
|
||||
let result = self.inner_post(env, raw, _dry_run);
|
||||
if let Err(err) = result {
|
||||
return match self.insert_to_error_queue(env, raw, err.to_string()) {
|
||||
Ok(idx) => Err(Error::from_kind(Information(format!(
|
||||
"Inserted into error_queue at index {}",
|
||||
idx
|
||||
)))
|
||||
.chain_err(|| err)),
|
||||
Err(err2) => Err(err.chain_err(|| err2)),
|
||||
};
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn inner_post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
|
||||
trace!("Received envelope to post: {:#?}", &env);
|
||||
let tos = env.to().to_vec();
|
||||
if tos.is_empty() {
|
||||
return Err("Envelope To: field is empty!".into());
|
||||
}
|
||||
if env.from().is_empty() {
|
||||
return Err("Envelope From: field is empty!".into());
|
||||
}
|
||||
let mut lists = self.lists()?;
|
||||
for t in &tos {
|
||||
if let Some((addr, subaddr)) = t.subaddress("+") {
|
||||
lists.retain(|list| {
|
||||
if !addr.contains_address(&list.address()) {
|
||||
return true;
|
||||
}
|
||||
if let Err(err) = ListRequest::try_from((subaddr.as_str(), env))
|
||||
.and_then(|req| self.request(list, req, env, raw))
|
||||
{
|
||||
info!("Processing request returned error: {}", err);
|
||||
}
|
||||
false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
lists.retain(|list| {
|
||||
trace!(
|
||||
"Is post related to list {}? {}",
|
||||
&list,
|
||||
tos.iter().any(|a| a.contains_address(&list.address()))
|
||||
);
|
||||
|
||||
tos.iter().any(|a| a.contains_address(&list.address()))
|
||||
});
|
||||
if lists.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
trace!("Configuration is {:#?}", &self.conf);
|
||||
use crate::mail::{ListContext, Post, PostAction};
|
||||
for mut list in lists {
|
||||
trace!("Examining list {}", list.display_name());
|
||||
let filters = self.list_filters(&list);
|
||||
let memberships = self.list_members(list.pk)?;
|
||||
let owners = self.list_owners(list.pk)?;
|
||||
trace!("List members {:#?}", &memberships);
|
||||
let mut list_ctx = ListContext {
|
||||
policy: self.list_policy(list.pk)?,
|
||||
list_owners: &owners,
|
||||
list: &mut list,
|
||||
memberships: &memberships,
|
||||
scheduled_jobs: vec![],
|
||||
};
|
||||
let mut post = Post {
|
||||
from: env.from()[0].clone(),
|
||||
bytes: raw.to_vec(),
|
||||
to: env.to().to_vec(),
|
||||
action: PostAction::Hold,
|
||||
};
|
||||
let result = filters
|
||||
.into_iter()
|
||||
.fold(Ok((&mut post, &mut list_ctx)), |p, f| {
|
||||
p.and_then(|(p, c)| f.feed(p, c))
|
||||
});
|
||||
trace!("result {:#?}", result);
|
||||
|
||||
let Post { bytes, action, .. } = post;
|
||||
trace!("Action is {:#?}", action);
|
||||
let post_env = melib::Envelope::from_bytes(&bytes, None)?;
|
||||
match action {
|
||||
PostAction::Accept => {
|
||||
let _post_pk = self.insert_post(list_ctx.list.pk, &bytes, &post_env)?;
|
||||
trace!("post_pk is {:#?}", _post_pk);
|
||||
for job in list_ctx.scheduled_jobs.iter() {
|
||||
trace!("job is {:#?}", &job);
|
||||
if let crate::mail::MailJob::Send { recipients } = job {
|
||||
trace!("recipients: {:?}", &recipients);
|
||||
if !recipients.is_empty() {
|
||||
if let crate::config::SendMail::Smtp(ref smtp_conf) =
|
||||
&self.conf.send_mail
|
||||
{
|
||||
let smtp_conf = smtp_conf.clone();
|
||||
use melib::futures;
|
||||
use melib::smol;
|
||||
use melib::smtp::*;
|
||||
let mut conn = smol::future::block_on(smol::spawn(
|
||||
SmtpConnection::new_connection(smtp_conf.clone()),
|
||||
))?;
|
||||
futures::executor::block_on(conn.mail_transaction(
|
||||
&String::from_utf8_lossy(&bytes),
|
||||
Some(recipients),
|
||||
))?;
|
||||
}
|
||||
} else {
|
||||
trace!("list has no recipients");
|
||||
}
|
||||
}
|
||||
}
|
||||
/* - FIXME Save digest metadata in database */
|
||||
}
|
||||
PostAction::Reject { reason } => {
|
||||
/* FIXME - Notify submitter */
|
||||
trace!("PostAction::Reject {{ reason: {} }}", reason);
|
||||
//futures::executor::block_on(conn.mail_transaction(&post.bytes, b)).unwrap();
|
||||
return Err(PostRejected(reason).into());
|
||||
}
|
||||
PostAction::Defer { reason } => {
|
||||
trace!("PostAction::Defer {{ reason: {} }}", reason);
|
||||
/* - FIXME Notify submitter
|
||||
* - FIXME Save in database */
|
||||
}
|
||||
PostAction::Hold => {
|
||||
trace!("PostAction::Hold");
|
||||
/* FIXME - Save in database */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process a new mailing list request.
|
||||
pub fn request(
|
||||
&self,
|
||||
list: &DbVal<MailingList>,
|
||||
request: ListRequest,
|
||||
env: &Envelope,
|
||||
_raw: &[u8],
|
||||
) -> Result<()> {
|
||||
match request {
|
||||
ListRequest::Subscribe => {
|
||||
trace!(
|
||||
"subscribe action for addresses {:?} in list {}",
|
||||
env.from(),
|
||||
list
|
||||
);
|
||||
|
||||
let list_policy = self.list_policy(list.pk)?;
|
||||
let approval_needed = list_policy
|
||||
.as_ref()
|
||||
.map(|p| p.approval_needed)
|
||||
.unwrap_or(false);
|
||||
for f in env.from() {
|
||||
let membership = ListMembership {
|
||||
pk: 0,
|
||||
list: list.pk,
|
||||
address: f.get_email(),
|
||||
name: f.get_display_name(),
|
||||
digest: false,
|
||||
hide_address: false,
|
||||
receive_duplicates: true,
|
||||
receive_own_posts: false,
|
||||
receive_confirmation: true,
|
||||
enabled: !approval_needed,
|
||||
};
|
||||
if approval_needed {
|
||||
match self.add_candidate_member(list.pk, membership) {
|
||||
Ok(_) => {}
|
||||
Err(_err) => {}
|
||||
}
|
||||
//FIXME: send notification to list-owner
|
||||
} else if let Err(_err) = self.add_member(list.pk, membership) {
|
||||
//FIXME: send failure notice to f
|
||||
} else {
|
||||
//FIXME: send success notice
|
||||
}
|
||||
}
|
||||
}
|
||||
ListRequest::Unsubscribe => {
|
||||
trace!(
|
||||
"unsubscribe action for addresses {:?} in list {}",
|
||||
env.from(),
|
||||
list
|
||||
);
|
||||
for f in env.from() {
|
||||
if let Err(_err) = self.remove_membership(list.pk, &f.get_email()) {
|
||||
//FIXME: send failure notice to f
|
||||
} else {
|
||||
//FIXME: send success notice to f
|
||||
}
|
||||
}
|
||||
}
|
||||
ListRequest::Other(ref req) if req == "owner" => {
|
||||
trace!(
|
||||
"list-owner mail action for addresses {:?} in list {}",
|
||||
env.from(),
|
||||
list
|
||||
);
|
||||
//FIXME: mail to list-owner
|
||||
}
|
||||
ListRequest::RetrieveMessages(ref message_ids) => {
|
||||
trace!(
|
||||
"retrieve messages {:?} action for addresses {:?} in list {}",
|
||||
message_ids,
|
||||
env.from(),
|
||||
list
|
||||
);
|
||||
//FIXME
|
||||
}
|
||||
ListRequest::RetrieveArchive(ref from, ref to) => {
|
||||
trace!(
|
||||
"retrieve archie action from {:?} to {:?} for addresses {:?} in list {}",
|
||||
from,
|
||||
to,
|
||||
env.from(),
|
||||
list
|
||||
);
|
||||
//FIXME
|
||||
}
|
||||
ListRequest::SetDigest(ref toggle) => {
|
||||
trace!(
|
||||
"set digest action with value {} for addresses {:?} in list {}",
|
||||
toggle,
|
||||
env.from(),
|
||||
list
|
||||
);
|
||||
}
|
||||
ListRequest::Other(ref req) => {
|
||||
trace!(
|
||||
"unknown request action {} for addresses {:?} in list {}",
|
||||
req,
|
||||
env.from(),
|
||||
list
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch all year and month values for which at least one post exists in `yyyy-mm` format.
|
||||
pub fn months(&self, list_pk: i64) -> Result<Vec<String>> {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post WHERE list = ?;",
|
||||
)?;
|
||||
let months_iter = stmt.query_map([list_pk], |row| {
|
||||
let val: String = row.get(0)?;
|
||||
Ok(val)
|
||||
})?;
|
||||
|
||||
let mut ret = vec![];
|
||||
for month in months_iter {
|
||||
let month = month?;
|
||||
ret.push(month);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Errors of this library.
|
||||
|
||||
pub use crate::anyhow::Context;
|
||||
pub use error_chain::ChainedError;
|
||||
|
||||
// Create the Error, ErrorKind, ResultExt, and Result types
|
||||
|
||||
error_chain! {
|
||||
errors {
|
||||
/// Post rejected.
|
||||
PostRejected(reason: String) {
|
||||
description("Post rejected")
|
||||
display("Your post has been rejected: {}", reason)
|
||||
}
|
||||
|
||||
/// An entry was not found in the database.
|
||||
NotFound(model: &'static str) {
|
||||
description("Not found")
|
||||
display("This {} is not present in the database.", model)
|
||||
}
|
||||
|
||||
/// A request was invalid.
|
||||
InvalidRequest(reason: String) {
|
||||
description("List request is invalid")
|
||||
display("Your list request has been found invalid: {}.", reason)
|
||||
}
|
||||
|
||||
/// An error happened and it was handled internally.
|
||||
Information(reason: String) {
|
||||
description("")
|
||||
display("{}.", reason)
|
||||
}
|
||||
}
|
||||
foreign_links {
|
||||
Logic(anyhow::Error) #[doc="Error returned from an external user initiated operation such as deserialization or I/O."];
|
||||
Sql(rusqlite::Error) #[doc="Error returned from sqlite3."];
|
||||
Io(::std::io::Error) #[doc="Error returned from internal I/O operations."];
|
||||
Melib(melib::error::Error) #[doc="Error returned from e-mail protocol operations from `melib` crate."];
|
||||
SerdeJson(serde_json::Error) #[doc="Error from deserializing JSON values."];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#![warn(missing_docs)]
|
||||
//! Mailing list manager library.
|
||||
//!
|
||||
//! ```
|
||||
//! use mailpot::{models::*, Configuration, Connection, SendMail};
|
||||
//! # use tempfile::TempDir;
|
||||
//!
|
||||
//! # let tmp_dir = TempDir::new().unwrap();
|
||||
//! # let db_path = tmp_dir.path().join("mpot.db");
|
||||
//! # let config = Configuration {
|
||||
//! # send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
//! # db_path: db_path.clone(),
|
||||
//! # data_path: tmp_dir.path().to_path_buf(),
|
||||
//! # };
|
||||
//! #
|
||||
//! # fn do_test(config: Configuration) -> mailpot::Result<()> {
|
||||
//! let db = Connection::open_or_create_db(config)?.trusted();
|
||||
//!
|
||||
//! // Create a new mailing list
|
||||
//! let list_pk = db.create_list(MailingList {
|
||||
//! pk: 0,
|
||||
//! name: "foobar chat".into(),
|
||||
//! id: "foo-chat".into(),
|
||||
//! address: "foo-chat@example.com".into(),
|
||||
//! description: None,
|
||||
//! archive_url: None,
|
||||
//! })?.pk;
|
||||
//!
|
||||
//! db.set_list_policy(
|
||||
//! PostPolicy {
|
||||
//! pk: 0,
|
||||
//! list: list_pk,
|
||||
//! announce_only: false,
|
||||
//! subscriber_only: true,
|
||||
//! approval_needed: false,
|
||||
//! no_subscriptions: false,
|
||||
//! custom: false,
|
||||
//! },
|
||||
//! )?;
|
||||
//!
|
||||
//! // Drop privileges; we can only process new e-mail and modify memberships from now on.
|
||||
//! let db = db.untrusted();
|
||||
//!
|
||||
//! assert_eq!(db.list_members(list_pk)?.len(), 0);
|
||||
//! assert_eq!(db.list_posts(list_pk, None)?.len(), 0);
|
||||
//!
|
||||
//! // Process a subscription request e-mail
|
||||
//! let subscribe_bytes = b"From: Name <user@example.com>
|
||||
//! To: <foo-chat+subscribe@example.com>
|
||||
//! Subject: subscribe
|
||||
//! Date: Thu, 29 Oct 2020 13:58:16 +0000
|
||||
//! Message-ID: <1@example.com>
|
||||
//!
|
||||
//! ";
|
||||
//! let envelope = melib::Envelope::from_bytes(subscribe_bytes, None)?;
|
||||
//! db.post(&envelope, subscribe_bytes, /* dry_run */ false)?;
|
||||
//!
|
||||
//! assert_eq!(db.list_members(list_pk)?.len(), 1);
|
||||
//! assert_eq!(db.list_posts(list_pk, None)?.len(), 0);
|
||||
//!
|
||||
//! // Process a post
|
||||
//! let post_bytes = b"From: Name <user@example.com>
|
||||
//! To: <foo-chat@example.com>
|
||||
//! Subject: my first post
|
||||
//! Date: Thu, 29 Oct 2020 14:01:09 +0000
|
||||
//! Message-ID: <2@example.com>
|
||||
//!
|
||||
//! Hello
|
||||
//! ";
|
||||
//! let envelope =
|
||||
//! melib::Envelope::from_bytes(post_bytes, None).expect("Could not parse message");
|
||||
//! db.post(&envelope, post_bytes, /* dry_run */ false)?;
|
||||
//!
|
||||
//! assert_eq!(db.list_members(list_pk)?.len(), 1);
|
||||
//! assert_eq!(db.list_posts(list_pk, None)?.len(), 1);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # do_test(config);
|
||||
//! ```
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
extern crate anyhow;
|
||||
|
||||
#[macro_use]
|
||||
pub extern crate serde;
|
||||
pub extern crate log;
|
||||
pub extern crate melib;
|
||||
pub extern crate serde_json;
|
||||
|
||||
use log::{info, trace};
|
||||
|
||||
mod config;
|
||||
pub mod mail;
|
||||
pub mod models;
|
||||
use models::*;
|
||||
mod db;
|
||||
mod errors;
|
||||
|
||||
pub use config::{Configuration, SendMail};
|
||||
pub use db::*;
|
||||
pub use errors::*;
|
||||
|
||||
/// A `mailto:` value.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct MailtoAddress {
|
||||
/// E-mail address.
|
||||
pub address: String,
|
||||
/// Optional subject value.
|
||||
pub subject: Option<String>,
|
||||
}
|
||||
|
||||
#[doc = include_str!("../../README.md")]
|
||||
#[cfg(doctest)]
|
||||
pub struct ReadmeDoctests;
|
|
@ -17,21 +17,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Types for processing new posts:
|
||||
//! [`PostFilter`](crate::message_filters::PostFilter), [`ListContext`],
|
||||
//! Types for processing new posts: [`PostFilter`](message_filters::PostFilter), [`ListContext`],
|
||||
//! [`MailJob`] and [`PostAction`].
|
||||
|
||||
use std::collections::HashMap;
|
||||
use super::*;
|
||||
use melib::Address;
|
||||
pub mod message_filters;
|
||||
|
||||
use log::trace;
|
||||
use melib::{Address, MessageID};
|
||||
|
||||
use crate::{
|
||||
models::{ListOwner, ListSubscription, MailingList, PostPolicy, SubscriptionPolicy},
|
||||
DbVal,
|
||||
};
|
||||
/// Post action returned from a list's
|
||||
/// [`PostFilter`](crate::message_filters::PostFilter) stack.
|
||||
/// Post action returned from a list's [`PostFilter`](message_filters::PostFilter) stack.
|
||||
#[derive(Debug)]
|
||||
pub enum PostAction {
|
||||
/// Add to `hold` queue.
|
||||
|
@ -50,49 +43,37 @@ pub enum PostAction {
|
|||
},
|
||||
}
|
||||
|
||||
/// List context passed to a list's
|
||||
/// [`PostFilter`](crate::message_filters::PostFilter) stack.
|
||||
/// List context passed to a list's [`PostFilter`](message_filters::PostFilter) stack.
|
||||
#[derive(Debug)]
|
||||
pub struct ListContext<'list> {
|
||||
/// Which mailing list a post was addressed to.
|
||||
pub list: &'list MailingList,
|
||||
/// The mailing list owners.
|
||||
pub list_owners: &'list [DbVal<ListOwner>],
|
||||
/// The mailing list subscriptions.
|
||||
pub subscriptions: &'list [DbVal<ListSubscription>],
|
||||
/// The mailing list memberships.
|
||||
pub memberships: &'list [DbVal<ListMembership>],
|
||||
/// The mailing list post policy.
|
||||
pub post_policy: Option<DbVal<PostPolicy>>,
|
||||
/// The mailing list subscription policy.
|
||||
pub subscription_policy: Option<DbVal<SubscriptionPolicy>>,
|
||||
/// The scheduled jobs added by each filter in a list's
|
||||
/// [`PostFilter`](crate::message_filters::PostFilter) stack.
|
||||
pub policy: Option<DbVal<PostPolicy>>,
|
||||
/// The scheduled jobs added by each filter in a list's [`PostFilter`](message_filters::PostFilter) stack.
|
||||
pub scheduled_jobs: Vec<MailJob>,
|
||||
/// Saved settings for message filters, which process a
|
||||
/// received e-mail before taking a final decision/action.
|
||||
pub filter_settings: HashMap<String, DbVal<serde_json::Value>>,
|
||||
}
|
||||
|
||||
/// Post to be considered by the list's
|
||||
/// [`PostFilter`](crate::message_filters::PostFilter) stack.
|
||||
pub struct PostEntry {
|
||||
/// Post to be considered by the list's [`PostFilter`](message_filters::PostFilter) stack.
|
||||
pub struct Post {
|
||||
/// `From` address of post.
|
||||
pub from: Address,
|
||||
/// Raw bytes of post.
|
||||
pub bytes: Vec<u8>,
|
||||
/// `To` addresses of post.
|
||||
pub to: Vec<Address>,
|
||||
/// Final action set by each filter in a list's
|
||||
/// [`PostFilter`](crate::message_filters::PostFilter) stack.
|
||||
/// Final action set by each filter in a list's [`PostFilter`](message_filters::PostFilter) stack.
|
||||
pub action: PostAction,
|
||||
/// Post's Message-ID
|
||||
pub message_id: MessageID,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for PostEntry {
|
||||
impl core::fmt::Debug for Post {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
fmt.debug_struct(stringify!(PostEntry))
|
||||
fmt.debug_struct("Post")
|
||||
.field("from", &self.from)
|
||||
.field("message_id", &self.message_id)
|
||||
.field("bytes", &format_args!("{} bytes", self.bytes.len()))
|
||||
.field("to", &self.to.as_slice())
|
||||
.field("action", &self.action)
|
||||
|
@ -100,8 +81,7 @@ impl core::fmt::Debug for PostEntry {
|
|||
}
|
||||
}
|
||||
|
||||
/// Scheduled jobs added to a [`ListContext`] by a list's
|
||||
/// [`PostFilter`](crate::message_filters::PostFilter) stack.
|
||||
/// Scheduled jobs added to a [`ListContext`] by a list's [`PostFilter`](message_filters::PostFilter) stack.
|
||||
#[derive(Debug)]
|
||||
pub enum MailJob {
|
||||
/// Send post to recipients.
|
||||
|
@ -134,20 +114,16 @@ pub enum MailJob {
|
|||
/// Type of mailing list request.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum ListRequest {
|
||||
/// Get help about a mailing list and its available interfaces.
|
||||
Help,
|
||||
/// Request subscription.
|
||||
Subscribe,
|
||||
/// Request removal of subscription.
|
||||
Unsubscribe,
|
||||
/// Request reception of list posts from a month-year range, inclusive.
|
||||
RetrieveArchive(String, String),
|
||||
/// Request reception of specific mailing list posts from `Message-ID`
|
||||
/// values.
|
||||
/// Request reception of specific mailing list posts from `Message-ID` values.
|
||||
RetrieveMessages(Vec<String>),
|
||||
/// Request change in subscription settings.
|
||||
/// See [`ListSubscription`].
|
||||
ChangeSetting(String, bool),
|
||||
/// Request change in digest preferences. (See [`ListMembership`])
|
||||
SetDigest(bool),
|
||||
/// Other type of request.
|
||||
Other(String),
|
||||
}
|
||||
|
@ -158,23 +134,22 @@ impl std::fmt::Display for ListRequest {
|
|||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> TryFrom<(S, &melib::Envelope)> for ListRequest {
|
||||
impl<S: AsRef<str>> std::convert::TryFrom<(S, &melib::Envelope)> for ListRequest {
|
||||
type Error = crate::Error;
|
||||
|
||||
fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result<Self, Self::Error> {
|
||||
let val = val.as_ref();
|
||||
Ok(match val {
|
||||
"subscribe" => Self::Subscribe,
|
||||
"request" if env.subject().trim() == "subscribe" => Self::Subscribe,
|
||||
"unsubscribe" => Self::Unsubscribe,
|
||||
"request" if env.subject().trim() == "unsubscribe" => Self::Unsubscribe,
|
||||
"help" => Self::Help,
|
||||
"request" if env.subject().trim() == "help" => Self::Help,
|
||||
"request" => Self::Other(env.subject().trim().to_string()),
|
||||
"subscribe" | "request" if env.subject().trim() == "subscribe" => {
|
||||
ListRequest::Subscribe
|
||||
}
|
||||
"unsubscribe" | "request" if env.subject().trim() == "unsubscribe" => {
|
||||
ListRequest::Unsubscribe
|
||||
}
|
||||
"request" => ListRequest::Other(env.subject().trim().to_string()),
|
||||
_ => {
|
||||
// [ref:TODO] add ChangeSetting parsing
|
||||
trace!("unknown action = {} for addresses {:?}", val, env.from(),);
|
||||
Self::Other(val.trim().to_string())
|
||||
ListRequest::Other(val.trim().to_string())
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![allow(clippy::result_unit_err)]
|
||||
|
||||
//! Filters to pass each mailing list post through. Filters are functions that implement the
|
||||
//! [`PostFilter`] trait that can:
|
||||
//!
|
||||
//! - transform post content.
|
||||
//! - modify the final [`PostAction`] to take.
|
||||
//! - modify the final scheduled jobs to perform. (See [`MailJob`]).
|
||||
//!
|
||||
//! Filters are executed in sequence like this:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! let result = filters
|
||||
//! .into_iter()
|
||||
//! .fold(Ok((&mut post, &mut list_ctx)), |p, f| {
|
||||
//! p.and_then(|(p, c)| f.feed(p, c))
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! so the processing stops at the first returned error.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Filter that modifies and/or verifies a post candidate. On rejection, return a string
|
||||
/// describing the error and optionally set `post.action` to `Reject` or `Defer`
|
||||
pub trait PostFilter {
|
||||
/// Feed post into the filter. Perform modifications to `post` and / or `ctx`, and return them
|
||||
/// with `Result::Ok` unless you want to the processing to stop and return an `Result::Err`.
|
||||
fn feed<'p, 'list>(
|
||||
self: Box<Self>,
|
||||
post: &'p mut Post,
|
||||
ctx: &'p mut ListContext<'list>,
|
||||
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()>;
|
||||
}
|
||||
|
||||
/// Check that submitter can post to list, for now it accepts everything.
|
||||
pub struct PostRightsCheck;
|
||||
impl PostFilter for PostRightsCheck {
|
||||
fn feed<'p, 'list>(
|
||||
self: Box<Self>,
|
||||
post: &'p mut Post,
|
||||
ctx: &'p mut ListContext<'list>,
|
||||
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
|
||||
trace!("Running PostRightsCheck filter");
|
||||
if let Some(ref policy) = ctx.policy {
|
||||
if policy.announce_only {
|
||||
trace!("post policy is announce_only");
|
||||
let owner_addresses = ctx
|
||||
.list_owners
|
||||
.iter()
|
||||
.map(|lo| lo.address())
|
||||
.collect::<Vec<Address>>();
|
||||
trace!("Owner addresses are: {:#?}", &owner_addresses);
|
||||
trace!("Envelope from is: {:?}", &post.from);
|
||||
if !owner_addresses.iter().any(|addr| *addr == post.from) {
|
||||
trace!("Envelope From does not include any owner");
|
||||
post.action = PostAction::Reject {
|
||||
reason: "You are not allowed to post on this list.".to_string(),
|
||||
};
|
||||
return Err(());
|
||||
}
|
||||
} else if policy.subscriber_only {
|
||||
trace!("post policy is subscriber_only");
|
||||
let email_from = post.from.get_email();
|
||||
trace!("post from is {:?}", &email_from);
|
||||
trace!("post memberships are {:#?}", &ctx.memberships);
|
||||
if !ctx.memberships.iter().any(|lm| lm.address == email_from) {
|
||||
trace!("Envelope from is not subscribed to this list");
|
||||
post.action = PostAction::Reject {
|
||||
reason: "Only subscribers can post to this list.".to_string(),
|
||||
};
|
||||
return Err(());
|
||||
}
|
||||
} else if policy.approval_needed {
|
||||
trace!("post policy says approval_needed");
|
||||
post.action = PostAction::Defer {
|
||||
reason: "Your posting has been deferred. Approval from the list's moderators is required before it is submitted.".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok((post, ctx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure message contains only `\r\n` line terminators, required by SMTP.
|
||||
pub struct FixCRLF;
|
||||
impl PostFilter for FixCRLF {
|
||||
fn feed<'p, 'list>(
|
||||
self: Box<Self>,
|
||||
post: &'p mut Post,
|
||||
ctx: &'p mut ListContext<'list>,
|
||||
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
|
||||
trace!("Running FixCRLF filter");
|
||||
use std::io::prelude::*;
|
||||
let mut new_vec = Vec::with_capacity(post.bytes.len());
|
||||
for line in post.bytes.lines() {
|
||||
new_vec.extend_from_slice(line.unwrap().as_bytes());
|
||||
new_vec.extend_from_slice(b"\r\n");
|
||||
}
|
||||
post.bytes = new_vec;
|
||||
Ok((post, ctx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add `List-*` headers
|
||||
pub struct AddListHeaders;
|
||||
impl PostFilter for AddListHeaders {
|
||||
fn feed<'p, 'list>(
|
||||
self: Box<Self>,
|
||||
post: &'p mut Post,
|
||||
ctx: &'p mut ListContext<'list>,
|
||||
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
|
||||
trace!("Running AddListHeaders filter");
|
||||
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
|
||||
let list_id = ctx.list.display_name();
|
||||
let sender = format!("<{}>", ctx.list.address);
|
||||
headers.push((&b"List-ID"[..], list_id.as_bytes()));
|
||||
headers.push((&b"Sender"[..], sender.as_bytes()));
|
||||
let list_post = ctx.list.post_header();
|
||||
let list_unsubscribe = ctx.list.unsubscribe_header();
|
||||
let list_archive = ctx.list.archive_header();
|
||||
if let Some(post) = list_post.as_ref() {
|
||||
headers.push((&b"List-Post"[..], post.as_bytes()));
|
||||
}
|
||||
if let Some(unsubscribe) = list_unsubscribe.as_ref() {
|
||||
headers.push((&b"List-Unsubscribe"[..], unsubscribe.as_bytes()));
|
||||
}
|
||||
if let Some(archive) = list_archive.as_ref() {
|
||||
headers.push((&b"List-Archive"[..], archive.as_bytes()));
|
||||
}
|
||||
let mut new_vec = Vec::with_capacity(
|
||||
headers
|
||||
.iter()
|
||||
.map(|(h, v)| h.len() + v.len() + ": \r\n".len())
|
||||
.sum::<usize>()
|
||||
+ "\r\n\r\n".len()
|
||||
+ body.len(),
|
||||
);
|
||||
for (h, v) in headers {
|
||||
new_vec.extend_from_slice(h);
|
||||
new_vec.extend_from_slice(b": ");
|
||||
new_vec.extend_from_slice(v);
|
||||
new_vec.extend_from_slice(b"\r\n");
|
||||
}
|
||||
new_vec.extend_from_slice(b"\r\n\r\n");
|
||||
new_vec.extend_from_slice(body);
|
||||
|
||||
post.bytes = new_vec;
|
||||
Ok((post, ctx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds `Archived-At` field, if configured.
|
||||
pub struct ArchivedAtLink;
|
||||
impl PostFilter for ArchivedAtLink {
|
||||
fn feed<'p, 'list>(
|
||||
self: Box<Self>,
|
||||
post: &'p mut Post,
|
||||
ctx: &'p mut ListContext<'list>,
|
||||
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
|
||||
trace!("Running ArchivedAtLink filter");
|
||||
Ok((post, ctx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Assuming there are no more changes to be done on the post, it finalizes which list members
|
||||
/// will receive the post in `post.action` field.
|
||||
pub struct FinalizeRecipients;
|
||||
impl PostFilter for FinalizeRecipients {
|
||||
fn feed<'p, 'list>(
|
||||
self: Box<Self>,
|
||||
post: &'p mut Post,
|
||||
ctx: &'p mut ListContext<'list>,
|
||||
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
|
||||
trace!("Running FinalizeRecipients filter");
|
||||
let mut recipients = vec![];
|
||||
let mut digests = vec![];
|
||||
let email_from = post.from.get_email();
|
||||
for member in ctx.memberships {
|
||||
trace!("examining member {:?}", &member);
|
||||
if member.address == email_from {
|
||||
trace!("member is submitter");
|
||||
}
|
||||
if member.digest {
|
||||
if member.address != email_from || member.receive_own_posts {
|
||||
trace!("Member gets digest");
|
||||
digests.push(member.address());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if member.address != email_from || member.receive_own_posts {
|
||||
trace!("Member gets copy");
|
||||
recipients.push(member.address());
|
||||
}
|
||||
// TODO:
|
||||
// - check for duplicates (To,Cc,Bcc)
|
||||
// - send confirmation to submitter
|
||||
}
|
||||
ctx.scheduled_jobs.push(MailJob::Send { recipients });
|
||||
if !digests.is_empty() {
|
||||
ctx.scheduled_jobs.push(MailJob::StoreDigest {
|
||||
recipients: digests,
|
||||
});
|
||||
}
|
||||
post.action = PostAction::Accept;
|
||||
Ok((post, ctx))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Database models: [`MailingList`], [`ListOwner`], [`ListMembership`], [`PostPolicy`] and
|
||||
//! [`Post`].
|
||||
|
||||
use super::*;
|
||||
pub mod changesets;
|
||||
|
||||
use melib::email::Address;
|
||||
|
||||
/// A database entry and its primary key. Derefs to its inner type.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct DbVal<T>(pub T, #[serde(skip)] pub i64);
|
||||
|
||||
impl<T> DbVal<T> {
|
||||
/// Primary key.
|
||||
#[inline(always)]
|
||||
pub fn pk(&self) -> i64 {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for DbVal<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Display for DbVal<T>
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A mailing list entry.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
pub struct MailingList {
|
||||
/// Database primary key.
|
||||
pub pk: i64,
|
||||
/// Mailing list name.
|
||||
pub name: String,
|
||||
/// Mailing list ID (what appears in the subject tag, e.g. `[mailing-list] New post!`).
|
||||
pub id: String,
|
||||
/// Mailing list e-mail address.
|
||||
pub address: String,
|
||||
/// Mailing list description.
|
||||
pub description: Option<String>,
|
||||
/// Mailing list archive URL.
|
||||
pub archive_url: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MailingList {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if let Some(description) = self.description.as_ref() {
|
||||
write!(
|
||||
fmt,
|
||||
"[#{} {}] {} <{}>: {}",
|
||||
self.pk, self.id, self.name, self.address, description
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
fmt,
|
||||
"[#{} {}] {} <{}>",
|
||||
self.pk, self.id, self.name, self.address
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailingList {
|
||||
/// Mailing list display name (e.g. `list name <list_address@example.com>`).
|
||||
pub fn display_name(&self) -> String {
|
||||
format!("\"{}\" <{}>", self.name, self.address)
|
||||
}
|
||||
|
||||
/// Value of `List-Post` header.
|
||||
///
|
||||
/// See RFC2369 Section 3.4: <https://www.rfc-editor.org/rfc/rfc2369#section-3.4>
|
||||
pub fn post_header(&self) -> Option<String> {
|
||||
Some(format!("<mailto:{}>", self.address))
|
||||
}
|
||||
|
||||
/// Value of `List-Unsubscribe` header.
|
||||
///
|
||||
/// See RFC2369 Section 3.2: <https://www.rfc-editor.org/rfc/rfc2369#section-3.2>
|
||||
pub fn unsubscribe_header(&self) -> Option<String> {
|
||||
let p = self.address.split('@').collect::<Vec<&str>>();
|
||||
Some(format!(
|
||||
"<mailto:{}-request@{}?subject=subscribe>",
|
||||
p[0], p[1]
|
||||
))
|
||||
}
|
||||
|
||||
/// Value of `List-Archive` header.
|
||||
///
|
||||
/// See RFC2369 Section 3.6: <https://www.rfc-editor.org/rfc/rfc2369#section-3.6>
|
||||
pub fn archive_header(&self) -> Option<String> {
|
||||
self.archive_url.as_ref().map(|url| format!("<{}>", url))
|
||||
}
|
||||
|
||||
/// List address as a [`melib::Address`]
|
||||
pub fn address(&self) -> Address {
|
||||
Address::new(Some(self.name.clone()), self.address.clone())
|
||||
}
|
||||
|
||||
/// List unsubscribe action as a [`MailtoAddress`](super::MailtoAddress).
|
||||
pub fn unsubscribe_mailto(&self) -> Option<MailtoAddress> {
|
||||
let p = self.address.split('@').collect::<Vec<&str>>();
|
||||
Some(MailtoAddress {
|
||||
address: format!("{}-request@{}", p[0], p[1]),
|
||||
subject: Some("unsubscribe".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
/// List subscribe action as a [`MailtoAddress`](super::MailtoAddress).
|
||||
pub fn subscribe_mailto(&self) -> Option<MailtoAddress> {
|
||||
let p = self.address.split('@').collect::<Vec<&str>>();
|
||||
Some(MailtoAddress {
|
||||
address: format!("{}-request@{}", p[0], p[1]),
|
||||
subject: Some("subscribe".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
/// List archive url value.
|
||||
pub fn archive_url(&self) -> Option<&str> {
|
||||
self.archive_url.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A mailing list membership entry.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ListMembership {
|
||||
/// Database primary key.
|
||||
pub pk: i64,
|
||||
/// Mailing list foreign key (See [`MailingList`]).
|
||||
pub list: i64,
|
||||
/// Member's e-mail address.
|
||||
pub address: String,
|
||||
/// Member's name, optional.
|
||||
pub name: Option<String>,
|
||||
/// Whether member wishes to receive list posts as a periodical digest e-mail.
|
||||
pub digest: bool,
|
||||
/// Whether member wishes their e-mail address hidden from public view.
|
||||
pub hide_address: bool,
|
||||
/// Whether member wishes to receive mailing list post duplicates, i.e. posts addressed to them
|
||||
/// and the mailing list to which they are subscribed.
|
||||
pub receive_duplicates: bool,
|
||||
/// Whether member wishes to receive their own mailing list posts from the mailing list, as a
|
||||
/// confirmation.
|
||||
pub receive_own_posts: bool,
|
||||
/// Whether member wishes to receive a plain confirmation for their own mailing list posts.
|
||||
pub receive_confirmation: bool,
|
||||
/// Whether this membership is enabled.
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListMembership {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
fmt,
|
||||
"{} [digest: {}, hide_address: {} {}]",
|
||||
self.address(),
|
||||
self.digest,
|
||||
self.hide_address,
|
||||
if self.enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"not enabled"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ListMembership {
|
||||
/// Member address as a [`melib::Address`]
|
||||
pub fn address(&self) -> Address {
|
||||
Address::new(self.name.clone(), self.address.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A mailing list post policy entry.
|
||||
///
|
||||
/// Only one of the boolean flags must be set to true.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PostPolicy {
|
||||
/// Database primary key.
|
||||
pub pk: i64,
|
||||
/// Mailing list foreign key (See [`MailingList`]).
|
||||
pub list: i64,
|
||||
/// Whether the policy is announce only (Only list owners can submit posts, and everyone will
|
||||
/// receive them).
|
||||
pub announce_only: bool,
|
||||
/// Whether the policy is "subscriber only" (Only list subscribers can post).
|
||||
pub subscriber_only: bool,
|
||||
/// Whether the policy is "approval needed" (Anyone can post, but approval from list owners is
|
||||
/// required if they are not subscribed).
|
||||
pub approval_needed: bool,
|
||||
/// Whether the policy is "no subscriptions" (Anyone can post, but approval from list owners is
|
||||
/// required. Subscriptions are not enabled).
|
||||
pub no_subscriptions: bool,
|
||||
/// Custom policy.
|
||||
pub custom: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PostPolicy {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A mailing list owner entry.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ListOwner {
|
||||
/// Database primary key.
|
||||
pub pk: i64,
|
||||
/// Mailing list foreign key (See [`MailingList`]).
|
||||
pub list: i64,
|
||||
/// Mailing list owner e-mail address.
|
||||
pub address: String,
|
||||
/// Mailing list owner name, optional.
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListOwner {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "[#{} {}] {}", self.pk, self.list, self.address())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListOwner> for ListMembership {
|
||||
fn from(val: ListOwner) -> ListMembership {
|
||||
ListMembership {
|
||||
pk: 0,
|
||||
list: val.list,
|
||||
address: val.address,
|
||||
name: val.name,
|
||||
digest: false,
|
||||
hide_address: false,
|
||||
receive_duplicates: true,
|
||||
receive_own_posts: false,
|
||||
receive_confirmation: true,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListOwner {
|
||||
/// Owner address as a [`melib::Address`]
|
||||
pub fn address(&self) -> Address {
|
||||
Address::new(self.name.clone(), self.address.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A mailing list post entry.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct Post {
|
||||
/// Database primary key.
|
||||
pub pk: i64,
|
||||
/// Mailing list foreign key (See [`MailingList`]).
|
||||
pub list: i64,
|
||||
/// `From` header address of post.
|
||||
pub address: String,
|
||||
/// `Message-ID` header value of post.
|
||||
pub message_id: String,
|
||||
/// Post as bytes.
|
||||
pub message: Vec<u8>,
|
||||
/// Unix timestamp of date.
|
||||
pub timestamp: u64,
|
||||
/// Datetime as string.
|
||||
pub datetime: String,
|
||||
/// Month-year as a `YYYY-mm` formatted string, for use in archives.
|
||||
pub month_year: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Post {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{:?}", self)
|
||||
}
|
||||
}
|
|
@ -19,18 +19,8 @@
|
|||
|
||||
//! Changeset structs: update specific struct fields.
|
||||
|
||||
macro_rules! impl_display {
|
||||
($t:ty) => {
|
||||
impl std::fmt::Display for $t {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{:?}", self)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Changeset struct for [`Mailinglist`](super::MailingList).
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct MailingListChangeset {
|
||||
/// Database primary key.
|
||||
pub pk: i64,
|
||||
|
@ -44,38 +34,20 @@ pub struct MailingListChangeset {
|
|||
pub description: Option<Option<String>>,
|
||||
/// Optional new value.
|
||||
pub archive_url: Option<Option<String>>,
|
||||
/// Optional new value.
|
||||
pub owner_local_part: Option<Option<String>>,
|
||||
/// Optional new value.
|
||||
pub request_local_part: Option<Option<String>>,
|
||||
/// Optional new value.
|
||||
pub verify: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub hidden: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl_display!(MailingListChangeset);
|
||||
|
||||
/// Changeset struct for [`ListSubscription`](super::ListSubscription).
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ListSubscriptionChangeset {
|
||||
/// Changeset struct for [`ListMembership`](super::ListMembership).
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ListMembershipChangeset {
|
||||
/// Mailing list foreign key (See [`MailingList`](super::MailingList)).
|
||||
pub list: i64,
|
||||
/// Subscription e-mail address.
|
||||
/// Membership e-mail address.
|
||||
pub address: String,
|
||||
/// Optional new value.
|
||||
pub account: Option<Option<i64>>,
|
||||
/// Optional new value.
|
||||
pub name: Option<Option<String>>,
|
||||
/// Optional new value.
|
||||
pub digest: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub enabled: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub verified: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub hide_address: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub receive_duplicates: Option<bool>,
|
||||
|
@ -83,12 +55,27 @@ pub struct ListSubscriptionChangeset {
|
|||
pub receive_own_posts: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub receive_confirmation: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl_display!(ListSubscriptionChangeset);
|
||||
/// Changeset struct for [`PostPolicy`](super::PostPolicy).
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PostPolicyChangeset {
|
||||
/// Database primary key.
|
||||
pub pk: i64,
|
||||
/// Mailing list foreign key (See [`MailingList`](super::MailingList)).
|
||||
pub list: i64,
|
||||
/// Optional new value.
|
||||
pub announce_only: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub subscriber_only: Option<bool>,
|
||||
/// Optional new value.
|
||||
pub approval_needed: Option<bool>,
|
||||
}
|
||||
|
||||
/// Changeset struct for [`ListOwner`](super::ListOwner).
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ListOwnerChangeset {
|
||||
/// Database primary key.
|
||||
pub pk: i64,
|
||||
|
@ -100,21 +87,24 @@ pub struct ListOwnerChangeset {
|
|||
pub name: Option<Option<String>>,
|
||||
}
|
||||
|
||||
impl_display!(ListOwnerChangeset);
|
||||
|
||||
/// Changeset struct for [`Account`](super::Account).
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AccountChangeset {
|
||||
/// Account e-mail address.
|
||||
pub address: String,
|
||||
/// Optional new value.
|
||||
pub name: Option<Option<String>>,
|
||||
/// Optional new value.
|
||||
pub public_key: Option<Option<String>>,
|
||||
/// Optional new value.
|
||||
pub password: Option<String>,
|
||||
/// Optional new value.
|
||||
pub enabled: Option<Option<bool>>,
|
||||
impl std::fmt::Display for MailingListChangeset {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl_display!(AccountChangeset);
|
||||
impl std::fmt::Display for ListMembershipChangeset {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{:?}", self)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for PostPolicyChangeset {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{:?}", self)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for ListOwnerChangeset {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{:?}", self)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
PRAGMA foreign_keys = true;
|
||||
PRAGMA encoding = 'UTF-8';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mailing_lists (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
archive_url TEXT,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS list_owner (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
name TEXT,
|
||||
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS post_policy (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL UNIQUE,
|
||||
announce_only BOOLEAN CHECK (announce_only in (0, 1)) NOT NULL DEFAULT 0,
|
||||
subscriber_only BOOLEAN CHECK (subscriber_only in (0, 1)) NOT NULL DEFAULT 0,
|
||||
approval_needed BOOLEAN CHECK (approval_needed in (0, 1)) NOT NULL DEFAULT 0,
|
||||
no_subscriptions BOOLEAN CHECK (no_subscriptions in (0, 1)) NOT NULL DEFAULT 0,
|
||||
custom BOOLEAN CHECK (custom in (0, 1)) NOT NULL DEFAULT 0,
|
||||
CHECK(((custom) OR (((no_subscriptions) OR (((approval_needed) OR (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))) AND NOT ((approval_needed) AND (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))))) AND NOT ((no_subscriptions) AND (((approval_needed) OR (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))) AND NOT ((approval_needed) AND (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))))))) AND NOT ((custom) AND (((no_subscriptions) OR (((approval_needed) OR (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))) AND NOT ((approval_needed) AND (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))))) AND NOT ((no_subscriptions) AND (((approval_needed) OR (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))) AND NOT ((approval_needed) AND (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only))))))))),
|
||||
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS membership (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
name TEXT,
|
||||
account INTEGER,
|
||||
enabled BOOLEAN CHECK (enabled in (0, 1)) NOT NULL DEFAULT 1,
|
||||
digest BOOLEAN CHECK (digest in (0, 1)) NOT NULL DEFAULT 0,
|
||||
hide_address BOOLEAN CHECK (hide_address in (0, 1)) NOT NULL DEFAULT 0,
|
||||
receive_duplicates BOOLEAN CHECK (receive_duplicates in (0, 1)) NOT NULL DEFAULT 1,
|
||||
receive_own_posts BOOLEAN CHECK (receive_own_posts in (0, 1)) NOT NULL DEFAULT 0,
|
||||
receive_confirmation BOOLEAN CHECK (receive_confirmation in (0, 1)) NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE,
|
||||
FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE,
|
||||
UNIQUE (list, address) ON CONFLICT ROLLBACK
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT,
|
||||
address TEXT NOT NULL UNIQUE,
|
||||
public_key TEXT,
|
||||
password TEXT NOT NULL,
|
||||
enabled BOOLEAN CHECK (enabled in (0, 1)) NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS candidate_membership (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
name TEXT,
|
||||
accepted INTEGER,
|
||||
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE,
|
||||
FOREIGN KEY (accepted) REFERENCES membership(pk) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS post (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
message BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
datetime TEXT NOT NULL DEFAULT (datetime())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS post_event (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
post INTEGER NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
kind CHAR(1) CHECK (kind IN ('R', 'S', 'D', 'B', 'O')) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
FOREIGN KEY (post) REFERENCES post(pk) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS error_queue (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
error TEXT NOT NULL,
|
||||
to_address TEXT NOT NULL,
|
||||
from_address TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
message BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
datetime TEXT NOT NULL DEFAULT (datetime())
|
||||
);
|
||||
|
||||
-- # Queues
|
||||
--
|
||||
-- ## The "maildrop" queue
|
||||
--
|
||||
-- Messages that have been submitted but not yet processed, await processing in
|
||||
-- the "maildrop" queue. Messages can be added to the "maildrop" queue even when
|
||||
-- mailpot is not running.
|
||||
--
|
||||
-- ## The "deferred" queue
|
||||
--
|
||||
-- When all the deliverable recipients for a message are delivered, and for some
|
||||
-- recipients delivery failed for a transient reason (it might succeed later), the
|
||||
-- message is placed in the "deferred" queue.
|
||||
--
|
||||
-- ## The "hold" queue
|
||||
--
|
||||
-- List administrators may introduce rules for emails to be placed indefinitely in
|
||||
-- the "hold" queue. Messages placed in the "hold" queue stay there until the
|
||||
-- administrator intervenes. No periodic delivery attempts are made for messages
|
||||
-- in the "hold" queue.
|
||||
CREATE TABLE IF NOT EXISTS queue (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
kind TEXT CHECK (kind IN ('maildrop', 'hold', 'deferred', 'corrupt')) NOT NULL,
|
||||
to_addresses TEXT NOT NULL,
|
||||
from_address TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
message BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
datetime TEXT NOT NULL DEFAULT (datetime())
|
||||
);
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS post_listpk_idx ON post(list);
|
||||
CREATE INDEX IF NOT EXISTS post_msgid_idx ON post(message_id);
|
||||
CREATE INDEX IF NOT EXISTS mailing_lists_idx ON mailing_lists(id);
|
||||
CREATE INDEX IF NOT EXISTS membership_idx ON membership(address);
|
|
@ -0,0 +1,140 @@
|
|||
define(xor, `(($1) OR ($2)) AND NOT (($1) AND ($2))')dnl
|
||||
define(BOOLEAN_TYPE, `$1 BOOLEAN CHECK ($1 in (0, 1)) NOT NULL')dnl
|
||||
define(BOOLEAN_FALSE, `0')dnl
|
||||
define(BOOLEAN_TRUE, `1')dnl
|
||||
PRAGMA foreign_keys = true;
|
||||
PRAGMA encoding = 'UTF-8';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mailing_lists (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
archive_url TEXT,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS list_owner (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
name TEXT,
|
||||
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS post_policy (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL UNIQUE,
|
||||
BOOLEAN_TYPE(announce_only) DEFAULT BOOLEAN_FALSE(),
|
||||
BOOLEAN_TYPE(subscriber_only) DEFAULT BOOLEAN_FALSE(),
|
||||
BOOLEAN_TYPE(approval_needed) DEFAULT BOOLEAN_FALSE(),
|
||||
BOOLEAN_TYPE(no_subscriptions) DEFAULT BOOLEAN_FALSE(),
|
||||
BOOLEAN_TYPE(custom) DEFAULT BOOLEAN_FALSE(),
|
||||
CHECK(xor(custom, xor(no_subscriptions, xor(approval_needed, xor(announce_only, subscriber_only))))),
|
||||
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS membership (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
name TEXT,
|
||||
account INTEGER,
|
||||
BOOLEAN_TYPE(enabled) DEFAULT BOOLEAN_TRUE(),
|
||||
BOOLEAN_TYPE(digest) DEFAULT BOOLEAN_FALSE(),
|
||||
BOOLEAN_TYPE(hide_address) DEFAULT BOOLEAN_FALSE(),
|
||||
BOOLEAN_TYPE(receive_duplicates) DEFAULT BOOLEAN_TRUE(),
|
||||
BOOLEAN_TYPE(receive_own_posts) DEFAULT BOOLEAN_FALSE(),
|
||||
BOOLEAN_TYPE(receive_confirmation) DEFAULT BOOLEAN_TRUE(),
|
||||
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE,
|
||||
FOREIGN KEY (account) REFERENCES account(pk) ON DELETE CASCADE,
|
||||
UNIQUE (list, address) ON CONFLICT ROLLBACK
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT,
|
||||
address TEXT NOT NULL UNIQUE,
|
||||
public_key TEXT,
|
||||
password TEXT NOT NULL,
|
||||
BOOLEAN_TYPE(enabled) DEFAULT BOOLEAN_TRUE()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS candidate_membership (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
name TEXT,
|
||||
accepted INTEGER,
|
||||
FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE,
|
||||
FOREIGN KEY (accepted) REFERENCES membership(pk) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS post (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
list INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
message BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
datetime TEXT NOT NULL DEFAULT (datetime())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS post_event (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
post INTEGER NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
kind CHAR(1) CHECK (kind IN ('R', 'S', 'D', 'B', 'O')) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
FOREIGN KEY (post) REFERENCES post(pk) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS error_queue (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
error TEXT NOT NULL,
|
||||
to_address TEXT NOT NULL,
|
||||
from_address TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
message BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
datetime TEXT NOT NULL DEFAULT (datetime())
|
||||
);
|
||||
|
||||
-- # Queues
|
||||
--
|
||||
-- ## The "maildrop" queue
|
||||
--
|
||||
-- Messages that have been submitted but not yet processed, await processing in
|
||||
-- the "maildrop" queue. Messages can be added to the "maildrop" queue even when
|
||||
-- mailpot is not running.
|
||||
--
|
||||
-- ## The "deferred" queue
|
||||
--
|
||||
-- When all the deliverable recipients for a message are delivered, and for some
|
||||
-- recipients delivery failed for a transient reason (it might succeed later), the
|
||||
-- message is placed in the "deferred" queue.
|
||||
--
|
||||
-- ## The "hold" queue
|
||||
--
|
||||
-- List administrators may introduce rules for emails to be placed indefinitely in
|
||||
-- the "hold" queue. Messages placed in the "hold" queue stay there until the
|
||||
-- administrator intervenes. No periodic delivery attempts are made for messages
|
||||
-- in the "hold" queue.
|
||||
CREATE TABLE IF NOT EXISTS queue (
|
||||
pk INTEGER PRIMARY KEY NOT NULL,
|
||||
kind TEXT CHECK (kind IN ('maildrop', 'hold', 'deferred', 'corrupt')) NOT NULL,
|
||||
to_addresses TEXT NOT NULL,
|
||||
from_address TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
message BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
datetime TEXT NOT NULL DEFAULT (datetime())
|
||||
);
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS post_listpk_idx ON post(list);
|
||||
CREATE INDEX IF NOT EXISTS post_msgid_idx ON post(message_id);
|
||||
CREATE INDEX IF NOT EXISTS mailing_lists_idx ON mailing_lists(id);
|
||||
CREATE INDEX IF NOT EXISTS membership_idx ON membership(address);
|
|
@ -17,21 +17,22 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use mailpot::{models::*, Configuration, Connection, ErrorKind, SendMail};
|
||||
use mailpot_tests::init_stderr_logging;
|
||||
mod utils;
|
||||
|
||||
use mailpot::{models::*, Configuration, Connection, SendMail};
|
||||
use std::error::Error;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_authorizer() {
|
||||
init_stderr_logging();
|
||||
utils::init_stderr_logging();
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
db_path,
|
||||
db_path: db_path.clone(),
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
administrators: vec![],
|
||||
};
|
||||
|
||||
let db = Connection::open_or_create_db(config).unwrap();
|
||||
|
@ -44,33 +45,31 @@ fn test_authorizer() {
|
|||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap_err(),
|
||||
db.remove_list_owner(1, 1).unwrap_err(),
|
||||
db.remove_list_post_policy(1, 1).unwrap_err(),
|
||||
db.set_list_post_policy(PostPolicy {
|
||||
db.remove_list_policy(1, 1).unwrap_err(),
|
||||
db.set_list_policy(PostPolicy {
|
||||
pk: 0,
|
||||
list: 1,
|
||||
announce_only: false,
|
||||
subscription_only: true,
|
||||
subscriber_only: true,
|
||||
approval_needed: false,
|
||||
open: false,
|
||||
no_subscriptions: false,
|
||||
custom: false,
|
||||
})
|
||||
.unwrap_err(),
|
||||
] {
|
||||
assert_eq!(
|
||||
err.kind().to_string(),
|
||||
ErrorKind::Sql(rusqlite::Error::SqliteFailure(
|
||||
rusqlite::ffi::Error {
|
||||
code: rusqlite::ErrorCode::AuthorizationForStatementDenied,
|
||||
extended_code: 23,
|
||||
},
|
||||
Some("not authorized".into()),
|
||||
))
|
||||
.to_string()
|
||||
err.source()
|
||||
.unwrap()
|
||||
.downcast_ref::<rusqlite::ffi::Error>()
|
||||
.unwrap(),
|
||||
&rusqlite::ffi::Error {
|
||||
code: rusqlite::ErrorCode::AuthorizationForStatementDenied,
|
||||
extended_code: 23
|
||||
},
|
||||
);
|
||||
}
|
||||
assert!(db.lists().unwrap().is_empty());
|
||||
|
@ -84,7 +83,6 @@ fn test_authorizer() {
|
|||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
archive_url: None,
|
||||
})
|
||||
.map(|_| ()),
|
||||
|
@ -95,17 +93,17 @@ fn test_authorizer() {
|
|||
name: None,
|
||||
})
|
||||
.map(|_| ()),
|
||||
db.set_list_post_policy(PostPolicy {
|
||||
db.set_list_policy(PostPolicy {
|
||||
pk: 0,
|
||||
list: 1,
|
||||
announce_only: false,
|
||||
subscription_only: true,
|
||||
subscriber_only: true,
|
||||
approval_needed: false,
|
||||
open: false,
|
||||
no_subscriptions: false,
|
||||
custom: false,
|
||||
})
|
||||
.map(|_| ()),
|
||||
db.remove_list_post_policy(1, 1).map(|_| ()),
|
||||
db.remove_list_policy(1, 1).map(|_| ()),
|
||||
db.remove_list_owner(1, 1).map(|_| ()),
|
||||
] {
|
||||
ok.unwrap();
|
|
@ -17,21 +17,21 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
mod utils;
|
||||
|
||||
use mailpot::{models::*, Configuration, Connection, SendMail};
|
||||
use mailpot_tests::init_stderr_logging;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_init_empty() {
|
||||
init_stderr_logging();
|
||||
utils::init_stderr_logging();
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
db_path,
|
||||
db_path: db_path.clone(),
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
administrators: vec![],
|
||||
};
|
||||
|
||||
let db = Connection::open_or_create_db(config).unwrap();
|
||||
|
@ -41,15 +41,14 @@ fn test_init_empty() {
|
|||
|
||||
#[test]
|
||||
fn test_list_creation() {
|
||||
init_stderr_logging();
|
||||
utils::init_stderr_logging();
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
db_path,
|
||||
db_path: db_path.clone(),
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
administrators: vec![],
|
||||
};
|
||||
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
|
@ -61,7 +60,6 @@ fn test_list_creation() {
|
|||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap();
|
|
@ -17,8 +17,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use mailpot::{melib, models::*, queue::Queue, Configuration, Connection, SendMail};
|
||||
use mailpot_tests::init_stderr_logging;
|
||||
mod utils;
|
||||
|
||||
use mailpot::{melib, models::*, Configuration, Connection, SendMail};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn get_smtp_conf() -> melib::smtp::SmtpServerConf {
|
||||
|
@ -35,15 +36,14 @@ fn get_smtp_conf() -> melib::smtp::SmtpServerConf {
|
|||
|
||||
#[test]
|
||||
fn test_error_queue() {
|
||||
init_stderr_logging();
|
||||
utils::init_stderr_logging();
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::Smtp(get_smtp_conf()),
|
||||
db_path,
|
||||
db_path: db_path.clone(),
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
administrators: vec![],
|
||||
};
|
||||
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
|
@ -55,42 +55,38 @@ fn test_error_queue() {
|
|||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(foo_chat.pk(), 1);
|
||||
let post_policy = db
|
||||
.set_list_post_policy(PostPolicy {
|
||||
.set_list_policy(PostPolicy {
|
||||
pk: 0,
|
||||
list: foo_chat.pk(),
|
||||
announce_only: false,
|
||||
subscription_only: true,
|
||||
subscriber_only: true,
|
||||
approval_needed: false,
|
||||
open: false,
|
||||
no_subscriptions: false,
|
||||
custom: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(post_policy.pk(), 1);
|
||||
assert_eq!(db.queue(Queue::Error).unwrap().len(), 0);
|
||||
assert_eq!(db.error_queue().unwrap().len(), 0);
|
||||
|
||||
// drop privileges
|
||||
let db = db.untrusted();
|
||||
|
||||
let input_bytes = include_bytes!("./test_sample_longmessage.eml");
|
||||
let envelope = melib::Envelope::from_bytes(input_bytes, None).expect("Could not parse message");
|
||||
db.post(&envelope, input_bytes, /* dry_run */ false)
|
||||
.expect("Got unexpected error");
|
||||
let out = db.queue(Queue::Out).unwrap();
|
||||
assert_eq!(out.len(), 1);
|
||||
const COMMENT_PREFIX: &str = "PostAction::Reject { reason: Only subscriptions";
|
||||
assert_eq!(
|
||||
out[0]
|
||||
.comment
|
||||
.as_ref()
|
||||
.and_then(|c| c.get(..COMMENT_PREFIX.len())),
|
||||
Some(COMMENT_PREFIX)
|
||||
);
|
||||
match db
|
||||
.post(&envelope, input_bytes, /* dry_run */ false)
|
||||
.unwrap_err()
|
||||
.kind()
|
||||
{
|
||||
mailpot::ErrorKind::PostRejected(_reason) => {}
|
||||
other => panic!("Got unexpected error: {}", other),
|
||||
}
|
||||
assert_eq!(db.error_queue().unwrap().len(), 1);
|
||||
}
|
|
@ -0,0 +1,410 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
mod utils;
|
||||
|
||||
use log::{trace, warn};
|
||||
use mailin_embedded::{Handler, Response, Server, SslConfig};
|
||||
use mailpot::{melib, models::*, Configuration, Connection, SendMail};
|
||||
use std::net::IpAddr; //, Ipv4Addr, Ipv6Addr};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const ADDRESS: &str = "127.0.0.1:8825";
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Helo,
|
||||
Mail {
|
||||
from: String,
|
||||
},
|
||||
Rcpt {
|
||||
from: String,
|
||||
to: Vec<String>,
|
||||
},
|
||||
DataStart {
|
||||
from: String,
|
||||
to: Vec<String>,
|
||||
},
|
||||
Data {
|
||||
#[allow(dead_code)]
|
||||
from: String,
|
||||
to: Vec<String>,
|
||||
buf: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MyHandler {
|
||||
mails: Arc<Mutex<Vec<((IpAddr, String), Message)>>>,
|
||||
stored: Arc<Mutex<Vec<(String, melib::Envelope)>>>,
|
||||
}
|
||||
use mailin_embedded::response::{INTERNAL_ERROR, OK};
|
||||
|
||||
impl Handler for MyHandler {
|
||||
fn helo(&mut self, ip: IpAddr, domain: &str) -> Response {
|
||||
// eprintln!("helo ip {:?} domain {:?}", ip, domain);
|
||||
self.mails
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(((ip, domain.to_string()), Message::Helo));
|
||||
OK
|
||||
}
|
||||
|
||||
fn mail(&mut self, ip: IpAddr, domain: &str, from: &str) -> Response {
|
||||
// eprintln!("mail() ip {:?} domain {:?} from {:?}", ip, domain, from);
|
||||
if let Some((_, message)) = self
|
||||
.mails
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter_mut()
|
||||
.find(|((i, d), _)| (i, d.as_str()) == (&ip, domain))
|
||||
{
|
||||
if let Message::Helo = message {
|
||||
*message = Message::Mail {
|
||||
from: from.to_string(),
|
||||
};
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
|
||||
fn rcpt(&mut self, _to: &str) -> Response {
|
||||
// eprintln!("rcpt() to {:?}", _to);
|
||||
if let Some((_, message)) = self.mails.lock().unwrap().last_mut() {
|
||||
if let Message::Mail { from } = message {
|
||||
*message = Message::Rcpt {
|
||||
from: from.clone(),
|
||||
to: vec![_to.to_string()],
|
||||
};
|
||||
return OK;
|
||||
} else if let Message::Rcpt { to, .. } = message {
|
||||
to.push(_to.to_string());
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
|
||||
fn data_start(
|
||||
&mut self,
|
||||
_domain: &str,
|
||||
_from: &str,
|
||||
_is8bit: bool,
|
||||
_to: &[String],
|
||||
) -> Response {
|
||||
// eprintln!( "data_start() domain {:?} from {:?} is8bit {:?} to {:?}", _domain, _from, _is8bit, _to);
|
||||
if let Some(((_, d), ref mut message)) = self.mails.lock().unwrap().last_mut() {
|
||||
if d != _domain {
|
||||
return INTERNAL_ERROR;
|
||||
}
|
||||
if let Message::Rcpt { from, to } = message {
|
||||
*message = Message::DataStart {
|
||||
from: from.to_string(),
|
||||
to: to.to_vec(),
|
||||
};
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
|
||||
fn data(&mut self, _buf: &[u8]) -> Result<(), std::io::Error> {
|
||||
if let Some(((_, _), ref mut message)) = self.mails.lock().unwrap().last_mut() {
|
||||
if let Message::DataStart { from, to } = message {
|
||||
*message = Message::Data {
|
||||
from: from.to_string(),
|
||||
to: to.clone(),
|
||||
buf: _buf.to_vec(),
|
||||
};
|
||||
return Ok(());
|
||||
} else if let Message::Data { buf, .. } = message {
|
||||
buf.extend(_buf.into_iter().copied());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data_end(&mut self) -> Response {
|
||||
//eprintln!("data_end()");
|
||||
if let Some(((_, _), message)) = self.mails.lock().unwrap().pop() {
|
||||
if let Message::Data { from: _, to, buf } = message {
|
||||
for to in to {
|
||||
match melib::Envelope::from_bytes(&buf, None) {
|
||||
Ok(env) => {
|
||||
self.stored.lock().unwrap().push((to.clone(), env));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("envelope parse error {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
fn get_smtp_conf() -> melib::smtp::SmtpServerConf {
|
||||
use melib::smtp::*;
|
||||
SmtpServerConf {
|
||||
hostname: "127.0.0.1".into(),
|
||||
port: 8825,
|
||||
envelope_from: "foo-chat@example.com".into(),
|
||||
auth: SmtpAuth::None,
|
||||
security: SmtpSecurity::None,
|
||||
extensions: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smtp() {
|
||||
utils::init_stderr_logging();
|
||||
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let handler = MyHandler {
|
||||
mails: Arc::new(Mutex::new(vec![])),
|
||||
stored: Arc::new(Mutex::new(vec![])),
|
||||
};
|
||||
let handler2 = handler.clone();
|
||||
let _smtp_handle = thread::spawn(move || {
|
||||
let mut server = Server::new(handler2);
|
||||
|
||||
server
|
||||
.with_name("example.com")
|
||||
.with_ssl(SslConfig::None)
|
||||
.unwrap()
|
||||
.with_addr(ADDRESS)
|
||||
.unwrap();
|
||||
eprintln!("Running smtp server at {}", ADDRESS);
|
||||
server.serve().expect("Could not run server");
|
||||
});
|
||||
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::Smtp(get_smtp_conf()),
|
||||
db_path: db_path.clone(),
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
};
|
||||
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
assert!(db.lists().unwrap().is_empty());
|
||||
let foo_chat = db
|
||||
.create_list(MailingList {
|
||||
pk: 0,
|
||||
name: "foobar chat".into(),
|
||||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(foo_chat.pk(), 1);
|
||||
let post_policy = db
|
||||
.set_list_policy(PostPolicy {
|
||||
pk: 0,
|
||||
list: foo_chat.pk(),
|
||||
announce_only: false,
|
||||
subscriber_only: true,
|
||||
approval_needed: false,
|
||||
no_subscriptions: false,
|
||||
custom: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(post_policy.pk(), 1);
|
||||
|
||||
let input_bytes = include_bytes!("./test_sample_longmessage.eml");
|
||||
match melib::Envelope::from_bytes(input_bytes, None) {
|
||||
Ok(envelope) => {
|
||||
// eprintln!("envelope {:?}", &envelope);
|
||||
match db
|
||||
.post(&envelope, input_bytes, /* dry_run */ false)
|
||||
.unwrap_err()
|
||||
.kind()
|
||||
{
|
||||
mailpot::ErrorKind::PostRejected(reason) => {
|
||||
trace!("Non-member post succesfully rejected: '{reason}'");
|
||||
}
|
||||
other => panic!("Got unexpected error: {}", other),
|
||||
}
|
||||
|
||||
db.add_member(
|
||||
foo_chat.pk(),
|
||||
ListMembership {
|
||||
pk: 0,
|
||||
list: foo_chat.pk(),
|
||||
address: "japoeunp@hotmail.com".into(),
|
||||
name: Some("Jamaica Poe".into()),
|
||||
digest: false,
|
||||
hide_address: false,
|
||||
receive_duplicates: true,
|
||||
receive_own_posts: true,
|
||||
receive_confirmation: true,
|
||||
enabled: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
db.add_member(
|
||||
foo_chat.pk(),
|
||||
ListMembership {
|
||||
pk: 0,
|
||||
list: foo_chat.pk(),
|
||||
address: "manos@example.com".into(),
|
||||
name: Some("Manos Hands".into()),
|
||||
digest: false,
|
||||
hide_address: false,
|
||||
receive_duplicates: true,
|
||||
receive_own_posts: true,
|
||||
receive_confirmation: true,
|
||||
enabled: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
db.post(&envelope, input_bytes, /* dry_run */ false)
|
||||
.unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Could not parse message: {}", err);
|
||||
}
|
||||
}
|
||||
assert_eq!(handler.stored.lock().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smtp_mailcrab() {
|
||||
use std::env;
|
||||
utils::init_stderr_logging();
|
||||
|
||||
fn get_smtp_conf() -> melib::smtp::SmtpServerConf {
|
||||
use melib::smtp::*;
|
||||
SmtpServerConf {
|
||||
hostname: "127.0.0.1".into(),
|
||||
port: 1025,
|
||||
envelope_from: "foo-chat@example.com".into(),
|
||||
auth: SmtpAuth::None,
|
||||
security: SmtpSecurity::None,
|
||||
extensions: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
let Ok(mailcrab_ip) = env::var("MAILCRAB_IP") else {
|
||||
warn!("MAILCRAB_IP env var not set, is mailcrab server running?");
|
||||
return;
|
||||
};
|
||||
let mailcrab_port = env::var("MAILCRAB_PORT").unwrap_or("1080".to_string());
|
||||
let api_uri = format!("http://{mailcrab_ip}:{mailcrab_port}/api/messages");
|
||||
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::Smtp(get_smtp_conf()),
|
||||
db_path: db_path.clone(),
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
};
|
||||
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
assert!(db.lists().unwrap().is_empty());
|
||||
let foo_chat = db
|
||||
.create_list(MailingList {
|
||||
pk: 0,
|
||||
name: "foobar chat".into(),
|
||||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(foo_chat.pk(), 1);
|
||||
let post_policy = db
|
||||
.set_list_policy(PostPolicy {
|
||||
pk: 0,
|
||||
list: foo_chat.pk(),
|
||||
announce_only: false,
|
||||
subscriber_only: true,
|
||||
approval_needed: false,
|
||||
no_subscriptions: false,
|
||||
custom: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(post_policy.pk(), 1);
|
||||
|
||||
let input_bytes = include_bytes!("./test_sample_longmessage.eml");
|
||||
match melib::Envelope::from_bytes(input_bytes, None) {
|
||||
Ok(envelope) => {
|
||||
match db
|
||||
.post(&envelope, input_bytes, /* dry_run */ false)
|
||||
.unwrap_err()
|
||||
.kind()
|
||||
{
|
||||
mailpot::ErrorKind::PostRejected(reason) => {
|
||||
trace!("Non-member post succesfully rejected: '{reason}'");
|
||||
}
|
||||
other => panic!("Got unexpected error: {}", other),
|
||||
}
|
||||
db.add_member(
|
||||
foo_chat.pk(),
|
||||
ListMembership {
|
||||
pk: 0,
|
||||
list: foo_chat.pk(),
|
||||
address: "japoeunp@hotmail.com".into(),
|
||||
name: Some("Jamaica Poe".into()),
|
||||
digest: false,
|
||||
hide_address: false,
|
||||
receive_duplicates: true,
|
||||
receive_own_posts: true,
|
||||
receive_confirmation: true,
|
||||
enabled: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
db.add_member(
|
||||
foo_chat.pk(),
|
||||
ListMembership {
|
||||
pk: 0,
|
||||
list: foo_chat.pk(),
|
||||
address: "manos@example.com".into(),
|
||||
name: Some("Manos Hands".into()),
|
||||
digest: false,
|
||||
hide_address: false,
|
||||
receive_duplicates: true,
|
||||
receive_own_posts: true,
|
||||
receive_confirmation: true,
|
||||
enabled: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
db.post(&envelope, input_bytes, /* dry_run */ false)
|
||||
.unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Could not parse message: {}", err);
|
||||
}
|
||||
}
|
||||
let mails: String = reqwest::blocking::get(&api_uri).unwrap().text().unwrap();
|
||||
trace!("mails: {}", mails);
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
mod utils;
|
||||
|
||||
use mailpot::{models::*, Configuration, Connection, SendMail};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_list_subscription() {
|
||||
utils::init_stderr_logging();
|
||||
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
db_path: db_path.clone(),
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
};
|
||||
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
assert!(db.lists().unwrap().is_empty());
|
||||
let foo_chat = db
|
||||
.create_list(MailingList {
|
||||
pk: 0,
|
||||
name: "foobar chat".into(),
|
||||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(foo_chat.pk(), 1);
|
||||
let lists = db.lists().unwrap();
|
||||
assert_eq!(lists.len(), 1);
|
||||
assert_eq!(lists[0], foo_chat);
|
||||
let post_policy = db
|
||||
.set_list_policy(PostPolicy {
|
||||
pk: 0,
|
||||
list: foo_chat.pk(),
|
||||
announce_only: false,
|
||||
subscriber_only: true,
|
||||
approval_needed: false,
|
||||
no_subscriptions: false,
|
||||
custom: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(post_policy.pk(), 1);
|
||||
assert_eq!(db.error_queue().unwrap().len(), 0);
|
||||
assert_eq!(db.list_members(foo_chat.pk()).unwrap().len(), 0);
|
||||
|
||||
let db = db.untrusted();
|
||||
|
||||
let input_bytes_1 = b"From: Name <user@example.com>
|
||||
To: <foo-chat@example.com>
|
||||
Subject: This is a post
|
||||
Date: Thu, 29 Oct 2020 13:58:16 +0000
|
||||
Message-ID:
|
||||
<PS1PR0601MB36750BD00EA89E1482FA98A2D5140@PS1PR0601MB3675.apcprd06.prod.outlook.com>
|
||||
Content-Language: en-US
|
||||
Content-Type: text/html
|
||||
Content-Transfer-Encoding: base64
|
||||
MIME-Version: 1.0
|
||||
|
||||
PCFET0NUWVBFPjxodG1sPjxoZWFkPjx0aXRsZT5mb288L3RpdGxlPjwvaGVhZD48Ym9k
|
||||
eT48dGFibGUgY2xhc3M9ImZvbyI+PHRoZWFkPjx0cj48dGQ+Zm9vPC90ZD48L3RoZWFk
|
||||
Pjx0Ym9keT48dHI+PHRkPmZvbzE8L3RkPjwvdHI+PC90Ym9keT48L3RhYmxlPjwvYm9k
|
||||
eT48L2h0bWw+
|
||||
";
|
||||
let envelope =
|
||||
melib::Envelope::from_bytes(input_bytes_1, None).expect("Could not parse message");
|
||||
match db
|
||||
.post(&envelope, input_bytes_1, /* dry_run */ false)
|
||||
.unwrap_err()
|
||||
.kind()
|
||||
{
|
||||
mailpot::ErrorKind::PostRejected(_reason) => {}
|
||||
other => panic!("Got unexpected error: {}", other),
|
||||
}
|
||||
assert_eq!(db.error_queue().unwrap().len(), 1);
|
||||
|
||||
let input_bytes_2 = b"From: Name <user@example.com>
|
||||
To: <foo-chat+subscribe@example.com>
|
||||
Subject: subscribe
|
||||
Date: Thu, 29 Oct 2020 13:58:16 +0000
|
||||
Message-ID:
|
||||
<PS1PR0601MB36750BD00EA89E1482FA98A2D5140_2@PS1PR0601MB3675.apcprd06.prod.outlook.com>
|
||||
Content-Language: en-US
|
||||
Content-Type: text/html
|
||||
Content-Transfer-Encoding: base64
|
||||
MIME-Version: 1.0
|
||||
|
||||
";
|
||||
let envelope =
|
||||
melib::Envelope::from_bytes(input_bytes_2, None).expect("Could not parse message");
|
||||
db.post(&envelope, input_bytes_2, /* dry_run */ false)
|
||||
.unwrap();
|
||||
assert_eq!(db.list_members(foo_chat.pk()).unwrap().len(), 1);
|
||||
assert_eq!(db.error_queue().unwrap().len(), 1);
|
||||
let envelope =
|
||||
melib::Envelope::from_bytes(input_bytes_1, None).expect("Could not parse message");
|
||||
db.post(&envelope, input_bytes_1, /* dry_run */ false)
|
||||
.unwrap();
|
||||
assert_eq!(db.error_queue().unwrap().len(), 1);
|
||||
assert_eq!(db.list_posts(foo_chat.pk(), None).unwrap().len(), 1);
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
Return-Path: <japoeunp@hotmail.com>
|
||||
Delivered-To: jonnny@miami-dice.co.uk
|
||||
Received: from violet.xenserver.co.uk
|
||||
by violet.xenserver.co.uk with LMTP
|
||||
id qBHcI7LKml9FxzIAYrQLqw
|
||||
(envelope-from <japoeunp@hotmail.com>)
|
||||
for <jonnny@miami-dice.co.uk>; Thu, 29 Oct 2020 13:59:14 +0000
|
||||
Return-path: <japoeunp@hotmail.com>
|
||||
Envelope-to: jonnny@miami-dice.co.uk
|
||||
Delivery-date: Thu, 29 Oct 2020 13:59:14 +0000
|
||||
Received: from mail-oln040092254105.outbound.protection.outlook.com ([40.92.254.105]:29481 helo=APC01-PU1-obe.outbound.protection.outlook.com)
|
||||
by violet.xenserver.co.uk with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
(Exim 4.93)
|
||||
(envelope-from <japoeunp@hotmail.com>)
|
||||
id 1kY8SJ-00DxYw-WD
|
||||
for jonnny@miami-dice.co.uk; Thu, 29 Oct 2020 13:59:14 +0000
|
||||
ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none;
|
||||
b=KKU/kthPXLl8CnAmBXXsD1QQWr4evL4ymaLwgHgRi5eSnOe2d2sQxrhcZ1VvLSvW2DQEQoNAm6NUtTC5uRUnBDS0n+g1E5/t1z8oFbzdioCIT6rL77ta3MVcaQ/o+gRa6dIwiNfu8z5GxAujOOu57gCfnCw3/gLeOHH01KtP4ezEB/DvAU9bC8eyso1T7nv+HT0riTjZOywGwDHnVb1aIPPIUiOQrrEi+cfLQRiCer01d94U8Wp+FUECrVYbr4uZGl8mbTwU4oZL1rJ25ubYG54e1ktaPJRa2YEitgJEF5sS8Z503c3RjzzBvvHkc/Kl6ypXcovP9xxeoSrS7YIPKA==
|
||||
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
|
||||
s=arcselector9901;
|
||||
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
|
||||
bh=NUSuxgSF4fNUuTts93/OAIsK9q9w8XhbybHWH/oRmXo=;
|
||||
b=VU2clBW8reAfnfCef0DeEDlBzcCU2u288YCjTvB0ekvBkJGSdI657WyS8KR7JSy0KcPWRfGbN9GJaETaasoa7bLdfuB6K9foup+vSqlA1witS5JQXQM/vJCKx67DbT8/8emLrKi7yDD2qjtRsb6HfvbwAGGvmPyUeyfTvRv6js+4YUbe5eN6CCdJEploBXDrWjFXHpSCwVCL1oF6rgrJf0+Td+ufX0QEHbOz2uJWj4yz0A8hK2yV+2JDVW7GiBwZMrO4yLNXYck/0HQRyYFe8I86xUBJWp/0IITCTe96x5L/H3lqmGkh4uRt8IsXT/2jBEm5CmXLxJZAMR8RONG9BQ==
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=hotmail.com;
|
||||
s=selector1;
|
||||
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
|
||||
bh=NUSuxgSF4fNUuTts93/OAIsK9q9w8XhbybHWH/oRmXo=;
|
||||
b=JRkih9HxwazdzH6MSzSetJMcRwvDr+e97VnoDCQYJf9qQqgtQvzMZR0Z+d2Gu74Ip3ebcvx5oYlOpV15yVZAqUmUeirpF2rdkmMWQiaDQMq9SLiF09eMDkDfEdGLD4V+C36QIISRamgyagIsC72/UB6OyxpXoAjP0SFxbyItvWVgB9EVVsSJLOKXWgRWiYSZxMLye3OQUqdWoiQ9Tw/o8uywLTvcojOizZaS2SrYWajYScBmMiCh58dUarKzrfXmR/WisfBepCf1ia7BKttjalhuJBcMyKfM923X5IbZ+Yw+gVpLtzwGUyPt2cobOAxKna11whmpWdtoBeXRR/hKOg==
|
||||
Received: from PU1APC01FT013.eop-APC01.prod.protection.outlook.com
|
||||
(2a01:111:e400:7ebe::45) by
|
||||
PU1APC01HT068.eop-APC01.prod.protection.outlook.com (2a01:111:e400:7ebe::323)
|
||||
with Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3520.15; Thu, 29 Oct
|
||||
2020 13:58:16 +0000
|
||||
Received: from PS1PR0601MB3675.apcprd06.prod.outlook.com
|
||||
(2a01:111:e400:7ebe::44) by PU1APC01FT013.mail.protection.outlook.com
|
||||
(2a01:111:e400:7ebe::78) with Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3520.15 via Frontend
|
||||
Transport; Thu, 29 Oct 2020 13:58:16 +0000
|
||||
Received: from PS1PR0601MB3675.apcprd06.prod.outlook.com
|
||||
([fe80::65ed:e320:1c31:1695]) by PS1PR0601MB3675.apcprd06.prod.outlook.com
|
||||
([fe80::65ed:e320:1c31:1695%7]) with mapi id 15.20.3499.027; Thu, 29 Oct 2020
|
||||
13:58:16 +0000
|
||||
From: Jamaica Poe <japoeunp@hotmail.com>
|
||||
To: <foo-chat@example.com>
|
||||
Subject: thankful that I had the chance to written report, that I could learn
|
||||
and let alone the chance $4454.32
|
||||
Thread-Topic: thankful that I had the chance to written report, that I could
|
||||
learn and let alone the chance $4454.32
|
||||
Thread-Index: AQHWrfuHFQ6EC5DxDEG0hktDfP8BQg==
|
||||
Date: Thu, 29 Oct 2020 13:58:16 +0000
|
||||
Message-ID:
|
||||
<PS1PR0601MB36750BD00EA89E1482FA98A2D5140@PS1PR0601MB3675.apcprd06.prod.outlook.com>
|
||||
Accept-Language: en-US
|
||||
Content-Language: en-US
|
||||
Content-Type: text/html
|
||||
Content-Transfer-Encoding: base64
|
||||
MIME-Version: 1.0
|
||||
|
||||
PCFET0NUWVBFPjxodG1sPjxoZWFkPjx0aXRsZT5mb288L3RpdGxlPjwvaGVhZD48Ym9k
|
||||
eT48dGFibGUgY2xhc3M9ImZvbyI+PHRoZWFkPjx0cj48dGQ+Zm9vPC90ZD48L3RoZWFk
|
||||
Pjx0Ym9keT48dHI+PHRkPmZvbzE8L3RkPjwvdHI+PC90Ym9keT48L3RhYmxlPjwvYm9k
|
||||
eT48L2h0bWw+
|
|
@ -17,13 +17,18 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
extern crate base64;
|
||||
extern crate ureq;
|
||||
pub use std::path::PathBuf;
|
||||
use std::sync::Once;
|
||||
|
||||
mod args;
|
||||
pub mod commands;
|
||||
pub mod import;
|
||||
pub mod lints;
|
||||
pub use args::*;
|
||||
pub use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||
static INIT_STDERR_LOGGING: Once = Once::new();
|
||||
|
||||
pub fn init_stderr_logging() {
|
||||
INIT_STDERR_LOGGING.call_once(|| {
|
||||
stderrlog::new()
|
||||
.quiet(false)
|
||||
.verbosity(15)
|
||||
.show_module_names(true)
|
||||
.timestamp(stderrlog::Timestamp::Millisecond)
|
||||
.init()
|
||||
.unwrap();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
|
||||
.Bl -tag -width Ds -compact -offset indent
|
||||
.It Ic dump-database
|
||||
Dumps database data to STDOUT.
|
||||
.It Ic list-lists
|
||||
Lists all registered mailing lists.
|
||||
.It Ic list
|
||||
Mailing list management.
|
||||
.It Ic create-list
|
||||
.Fl -name Ar name
|
||||
List name.
|
||||
|
||||
.Fl -id Ar id
|
||||
List ID.
|
||||
|
||||
.Fl -address Ar address
|
||||
List e-mail address.
|
||||
|
||||
.Fl -description Ar description
|
||||
List description.
|
||||
|
||||
.Fl -archive-url Ar archive-url
|
||||
List archive URL.
|
||||
|
||||
Create new list.
|
||||
.It Ic post
|
||||
.Fl -dry-run
|
||||
.
|
||||
|
||||
Post message from STDIN to list.
|
||||
.It Ic error-queue
|
||||
Mail that has not been handled properly end up in the error queue.
|
||||
.It Ic import-maildir
|
||||
.Fl -maildir-path Ar maildir-path
|
||||
.
|
||||
|
||||
Import a maildir folder into an existing list.
|
||||
.It Ic dump-database
|
||||
Dumps database data to STDOUT.
|
||||
.It Ic list-lists
|
||||
Lists all registered mailing lists.
|
||||
.It Ic list
|
||||
Mailing list management.
|
||||
.It Ic create-list
|
||||
Create new list.
|
||||
.It Ic post
|
||||
Post message from STDIN to list.
|
||||
.It Ic error-queue
|
||||
Mail that has not been handled properly end up in the error queue.
|
||||
.It Ic import-maildir
|
||||
Import a maildir folder into an existing list.
|
||||
.El
|
||||
.Pp
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
.Bl -tag -width Ds -compact -offset indent
|
||||
.It Ic list
|
||||
List.
|
||||
.It Ic print
|
||||
.Fl -index Ar index ...
|
||||
index of entry.
|
||||
|
||||
.Fl -json
|
||||
JSON format.
|
||||
|
||||
Print entry in RFC5322 or JSON format.
|
||||
.It Ic delete
|
||||
.Fl -index Ar index ...
|
||||
index of entry.
|
||||
|
||||
.Fl -quiet
|
||||
Do not print in stdout.
|
||||
|
||||
Delete entry and print it in stdout.
|
||||
.It Ic list
|
||||
List.
|
||||
.It Ic print
|
||||
Print entry in RFC5322 or JSON format.
|
||||
.It Ic delete
|
||||
Delete entry and print it in stdout.
|
||||
.El
|
||||
.Pp
|
|
@ -0,0 +1,2 @@
|
|||
.Sh AUTHORS
|
||||
Manos Pitsidianakis <epilys@nessuent.xyz>
|
|
@ -0,0 +1,6 @@
|
|||
.Dd $Mdocdate$
|
||||
.Dt MAILPOT 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm mailpot
|
||||
.Nd mini mailing list manager.
|
|
@ -0,0 +1,110 @@
|
|||
|
||||
.Bl -tag -width Ds -compact -offset indent
|
||||
.It Ic members
|
||||
List members of list.
|
||||
.It Ic add-member
|
||||
.Fl -address Ar address
|
||||
E-mail address.
|
||||
|
||||
.Fl -name Ar name
|
||||
Name.
|
||||
|
||||
.Fl -digest
|
||||
Send messages as digest?.
|
||||
|
||||
.Fl -hide-address
|
||||
Hide message from list when posting?.
|
||||
|
||||
.Fl -receive-confirmation Ar receive-confirmation
|
||||
Hide message from list when posting? Receive confirmation email when posting?.
|
||||
|
||||
.Fl -receive-duplicates Ar receive-duplicates
|
||||
Receive posts from list even if address exists in To or Cc header?.
|
||||
|
||||
.Fl -receive-own-posts Ar receive-own-posts
|
||||
Receive own posts from list?.
|
||||
|
||||
.Fl -enabled Ar enabled
|
||||
Is subscription enabled?.
|
||||
|
||||
Add member to list.
|
||||
.It Ic remove-member
|
||||
.Fl -address Ar address
|
||||
E-mail address.
|
||||
|
||||
Remove member from list.
|
||||
.It Ic update-membership
|
||||
Update membership info.
|
||||
.It Ic add-policy
|
||||
.Fl -announce-only
|
||||
.
|
||||
|
||||
.Fl -subscriber-only
|
||||
.
|
||||
|
||||
.Fl -approval-needed
|
||||
.
|
||||
|
||||
.Fl -no-subscriptions
|
||||
.
|
||||
|
||||
.Fl -custom
|
||||
.
|
||||
|
||||
Add policy to list.
|
||||
.It Ic remove-policy
|
||||
.Fl -pk Ar pk
|
||||
.
|
||||
|
||||
.
|
||||
.It Ic add-list-owner
|
||||
.Fl -address Ar address
|
||||
.
|
||||
|
||||
.Fl -name Ar name
|
||||
.
|
||||
|
||||
Add list owner to list.
|
||||
.It Ic remove-list-owner
|
||||
.Fl -pk Ar pk
|
||||
.
|
||||
|
||||
.
|
||||
.It Ic enable-membership
|
||||
Alias for update-membership --enabled true.
|
||||
.It Ic disable-membership
|
||||
Alias for update-membership --enabled false.
|
||||
.It Ic update
|
||||
Update mailing list details.
|
||||
.It Ic health
|
||||
Show mailing list health status.
|
||||
.It Ic info
|
||||
Show mailing list info.
|
||||
.It Ic members
|
||||
List members of list.
|
||||
.It Ic add-member
|
||||
Add member to list.
|
||||
.It Ic remove-member
|
||||
Remove member from list.
|
||||
.It Ic update-membership
|
||||
Update membership info.
|
||||
.It Ic add-policy
|
||||
Add policy to list.
|
||||
.It Ic remove-policy
|
||||
.
|
||||
.It Ic add-list-owner
|
||||
Add list owner to list.
|
||||
.It Ic remove-list-owner
|
||||
.
|
||||
.It Ic enable-membership
|
||||
Alias for update-membership --enabled true.
|
||||
.It Ic disable-membership
|
||||
Alias for update-membership --enabled false.
|
||||
.It Ic update
|
||||
Update mailing list details.
|
||||
.It Ic health
|
||||
Show mailing list health status.
|
||||
.It Ic info
|
||||
Show mailing list info.
|
||||
.El
|
||||
.Pp
|
|
@ -0,0 +1,229 @@
|
|||
.Dd $Mdocdate$
|
||||
.Dt MAILPOT 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm mailpot
|
||||
.Nd mini mailing list manager.
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl -debug
|
||||
.Op Fl -config Ar config
|
||||
.Op Fl -quiet | -q
|
||||
.Op Fl -verbose | -v
|
||||
.Op Fl -timestamp | -t Ar timestamp
|
||||
.Bl -tag -width flag -offset indent
|
||||
.It Fl -debug
|
||||
Activate debug mode.
|
||||
.It Fl -config Ar config
|
||||
Set config file.
|
||||
.It Fl -quiet | -q
|
||||
Silence all output.
|
||||
.It Fl -verbose | -v
|
||||
Verbose mode (-v, -vv, -vvv, etc).
|
||||
.It Fl -timestamp | -t Ar timestamp
|
||||
Timestamp (sec, ms, ns, none).
|
||||
.El
|
||||
|
||||
.Sh DESCRIPTION
|
||||
This command-line tool allows you to control databases of the mailing list manager
|
||||
.Nm Ns .
|
||||
.Pp
|
||||
.Sh COMMANDS
|
||||
|
||||
.Bl -tag -width Ds -compact -offset indent
|
||||
.It Ic dump-database
|
||||
Dumps database data to STDOUT.
|
||||
.It Ic list-lists
|
||||
Lists all registered mailing lists.
|
||||
.It Ic list
|
||||
Mailing list management.
|
||||
.It Ic create-list
|
||||
.Fl -name Ar name
|
||||
List name.
|
||||
|
||||
.Fl -id Ar id
|
||||
List ID.
|
||||
|
||||
.Fl -address Ar address
|
||||
List e-mail address.
|
||||
|
||||
.Fl -description Ar description
|
||||
List description.
|
||||
|
||||
.Fl -archive-url Ar archive-url
|
||||
List archive URL.
|
||||
|
||||
Create new list.
|
||||
.It Ic post
|
||||
.Fl -dry-run
|
||||
.
|
||||
|
||||
Post message from STDIN to list.
|
||||
.It Ic error-queue
|
||||
Mail that has not been handled properly end up in the error queue.
|
||||
.It Ic import-maildir
|
||||
.Fl -maildir-path Ar maildir-path
|
||||
.
|
||||
|
||||
Import a maildir folder into an existing list.
|
||||
.It Ic dump-database
|
||||
Dumps database data to STDOUT.
|
||||
.It Ic list-lists
|
||||
Lists all registered mailing lists.
|
||||
.It Ic list
|
||||
Mailing list management.
|
||||
.It Ic create-list
|
||||
Create new list.
|
||||
.It Ic post
|
||||
Post message from STDIN to list.
|
||||
.It Ic error-queue
|
||||
Mail that has not been handled properly end up in the error queue.
|
||||
.It Ic import-maildir
|
||||
Import a maildir folder into an existing list.
|
||||
.El
|
||||
.Pp
|
||||
|
||||
.Ss list subcommands
|
||||
|
||||
.Bl -tag -width Ds -compact -offset indent
|
||||
.It Ic members
|
||||
List members of list.
|
||||
.It Ic add-member
|
||||
.Fl -address Ar address
|
||||
E-mail address.
|
||||
|
||||
.Fl -name Ar name
|
||||
Name.
|
||||
|
||||
.Fl -digest
|
||||
Send messages as digest?.
|
||||
|
||||
.Fl -hide-address
|
||||
Hide message from list when posting?.
|
||||
|
||||
.Fl -receive-confirmation Ar receive-confirmation
|
||||
Hide message from list when posting? Receive confirmation email when posting?.
|
||||
|
||||
.Fl -receive-duplicates Ar receive-duplicates
|
||||
Receive posts from list even if address exists in To or Cc header?.
|
||||
|
||||
.Fl -receive-own-posts Ar receive-own-posts
|
||||
Receive own posts from list?.
|
||||
|
||||
.Fl -enabled Ar enabled
|
||||
Is subscription enabled?.
|
||||
|
||||
Add member to list.
|
||||
.It Ic remove-member
|
||||
.Fl -address Ar address
|
||||
E-mail address.
|
||||
|
||||
Remove member from list.
|
||||
.It Ic update-membership
|
||||
Update membership info.
|
||||
.It Ic add-policy
|
||||
.Fl -announce-only
|
||||
.
|
||||
|
||||
.Fl -subscriber-only
|
||||
.
|
||||
|
||||
.Fl -approval-needed
|
||||
.
|
||||
|
||||
.Fl -no-subscriptions
|
||||
.
|
||||
|
||||
.Fl -custom
|
||||
.
|
||||
|
||||
Add policy to list.
|
||||
.It Ic remove-policy
|
||||
.Fl -pk Ar pk
|
||||
.
|
||||
|
||||
.
|
||||
.It Ic add-list-owner
|
||||
.Fl -address Ar address
|
||||
.
|
||||
|
||||
.Fl -name Ar name
|
||||
.
|
||||
|
||||
Add list owner to list.
|
||||
.It Ic remove-list-owner
|
||||
.Fl -pk Ar pk
|
||||
.
|
||||
|
||||
.
|
||||
.It Ic enable-membership
|
||||
Alias for update-membership --enabled true.
|
||||
.It Ic disable-membership
|
||||
Alias for update-membership --enabled false.
|
||||
.It Ic update
|
||||
Update mailing list details.
|
||||
.It Ic health
|
||||
Show mailing list health status.
|
||||
.It Ic info
|
||||
Show mailing list info.
|
||||
.It Ic members
|
||||
List members of list.
|
||||
.It Ic add-member
|
||||
Add member to list.
|
||||
.It Ic remove-member
|
||||
Remove member from list.
|
||||
.It Ic update-membership
|
||||
Update membership info.
|
||||
.It Ic add-policy
|
||||
Add policy to list.
|
||||
.It Ic remove-policy
|
||||
.
|
||||
.It Ic add-list-owner
|
||||
Add list owner to list.
|
||||
.It Ic remove-list-owner
|
||||
.
|
||||
.It Ic enable-membership
|
||||
Alias for update-membership --enabled true.
|
||||
.It Ic disable-membership
|
||||
Alias for update-membership --enabled false.
|
||||
.It Ic update
|
||||
Update mailing list details.
|
||||
.It Ic health
|
||||
Show mailing list health status.
|
||||
.It Ic info
|
||||
Show mailing list info.
|
||||
.El
|
||||
.Pp
|
||||
|
||||
.Ss error-queue subcommands
|
||||
|
||||
.Bl -tag -width Ds -compact -offset indent
|
||||
.It Ic list
|
||||
List.
|
||||
.It Ic print
|
||||
.Fl -index Ar index ...
|
||||
index of entry.
|
||||
|
||||
.Fl -json
|
||||
JSON format.
|
||||
|
||||
Print entry in RFC5322 or JSON format.
|
||||
.It Ic delete
|
||||
.Fl -index Ar index ...
|
||||
index of entry.
|
||||
|
||||
.Fl -quiet
|
||||
Do not print in stdout.
|
||||
|
||||
Delete entry and print it in stdout.
|
||||
.It Ic list
|
||||
List.
|
||||
.It Ic print
|
||||
Print entry in RFC5322 or JSON format.
|
||||
.It Ic delete
|
||||
Delete entry and print it in stdout.
|
||||
.El
|
||||
.Pp
|
||||
|
||||
.Sh AUTHORS
|
||||
Manos Pitsidianakis <epilys@nessuent.xyz>
|
|
@ -0,0 +1,14 @@
|
|||
include(`docs/header.mdoc')
|
||||
.Sh SYNOPSIS
|
||||
include(`docs/main.mdoc')
|
||||
.Sh DESCRIPTION
|
||||
This command-line tool allows you to control databases of the mailing list manager
|
||||
.Nm Ns .
|
||||
.Pp
|
||||
.Sh COMMANDS
|
||||
include(`docs/command.mdoc')
|
||||
.Ss list subcommands
|
||||
include(`docs/list.mdoc')
|
||||
.Ss error-queue subcommands
|
||||
include(`docs/error_queue.mdoc')
|
||||
include(`docs/footer.mdoc')
|
|
@ -0,0 +1,18 @@
|
|||
.Nm
|
||||
.Op Fl -debug
|
||||
.Op Fl -config Ar config
|
||||
.Op Fl -quiet | -q
|
||||
.Op Fl -verbose | -v
|
||||
.Op Fl -timestamp | -t Ar timestamp
|
||||
.Bl -tag -width flag -offset indent
|
||||
.It Fl -debug
|
||||
Activate debug mode.
|
||||
.It Fl -config Ar config
|
||||
Set config file.
|
||||
.It Fl -quiet | -q
|
||||
Silence all output.
|
||||
.It Fl -verbose | -v
|
||||
Verbose mode (-v, -vv, -vvv, etc).
|
||||
.It Fl -timestamp | -t Ar timestamp
|
||||
Timestamp (sec, ms, ns, none).
|
||||
.El
|
1157
docs/mpot.1
1157
docs/mpot.1
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +0,0 @@
|
|||
# Relevant RFCs
|
||||
|
||||
The documents are gziped, to read them either do `gunzip GZ_FILE` or use a gzip compatible reader-editor, like vim (see `:help gzip`):
|
||||
|
||||
1. open vim
|
||||
2. `:set loaded_gzip = 1`
|
||||
3. `:e GZ_FILE`
|
|
@ -1,79 +0,0 @@
|
|||
## Search RFCs
|
||||
|
||||
[Advanced Search](/search/rfc_search.php)
|
||||
|
||||
# [RFC Editor](https://www.rfc-editor.org/)
|
||||
|
||||
#
|
||||
|
||||
## [RFC 1153](https://www.rfc-editor.org/rfc/rfc1153.txt)
|
||||
|
||||
### Digest message format, April 1990
|
||||
|
||||
**File formats:** [![icon for text
|
||||
file](/rfcscripts/images/RFC_ICONS_Text_40x50.png)](https://www.rfc-
|
||||
editor.org/rfc/rfc1153.txt) [![icon for
|
||||
PDF](/rfcscripts/images/RFC_ICONS_PDF_2019_50x40.png)](https://www.rfc-
|
||||
editor.org/rfc/pdfrfc/rfc1153.txt.pdf) [![icon for
|
||||
HTML](/rfcscripts/images/RFC_ICONS_HTML_40x50.png)](https://www.rfc-
|
||||
editor.org/rfc/rfc1153.html)
|
||||
|
||||
**Status:** EXPERIMENTAL
|
||||
|
||||
**Author:** F.J. Wancho
|
||||
|
||||
**Stream:** [Legacy]
|
||||
|
||||
**Cite this RFC** : [TXT](/refs/ref1153.txt) |**
|
||||
[XML](https://bib.ietf.org/public/rfc/bibxml/reference.RFC.1153.xml) |
|
||||
[BibTeX](https://datatracker.ietf.org/doc/rfc1153/bibtex/)
|
||||
|
||||
**DOI** : 10.17487/RFC1153
|
||||
|
||||
**Discuss this RFC** : Send questions or comments to the mailing list
|
||||
[iesg@ietf.org](mailto:iesg@ietf.org?subject=Question regarding RFC 1153 )
|
||||
|
||||
**Other actions** : [Submit Errata](/errata.php#reportnew) |** [ Find IPR
|
||||
Disclosures from the
|
||||
IETF](https://datatracker.ietf.org/ipr/search/?draft=&rfc=1153&submit=rfc)
|
||||
|** [ View History of RFC 1153](https://datatracker.ietf.org/doc/rfc1153/)
|
||||
|
||||
* * *
|
||||
|
||||
## Abstract
|
||||
|
||||
This memo describes the de facto standard Digest Message Format. This is an
|
||||
elective experimental protocol.
|
||||
|
||||
* * *
|
||||
|
||||
For the definition of **Status** , see [RFC 2026](/info/rfc2026).
|
||||
|
||||
For the definition of **Stream** , see [RFC 8729](/info/rfc8729).
|
||||
|
||||
* * *
|
||||
|
||||
|
||||
|
||||
|
||||
[IAB](//www.iab.org/) • [IANA](//www.iana.org/) • [IETF](//www.ietf.org) •
|
||||
[IRTF](//www.irtf.org) • [ISE](/about/independent) •
|
||||
[ISOC](//www.internetsociety.org) • [IETF Trust](//trustee.ietf.org/)
|
||||
[Reports](/report-summary) • [Privacy Statement](//www.ietf.org/privacy-
|
||||
statement/) • [Site Map](/sitemap) • [Contact Us](/contact)
|
||||
|
||||
[ ]()
|
||||
|
||||
* [Document Retrieval](https://www.rfc-editor.org/retrieve/)
|
||||
* [Errata](/errata.php)
|
||||
* [Frequently Asked Questions](https://www.rfc-editor.org/faq/)
|
||||
* [Future Format FAQ](https://www.rfc-editor.org/rse/format-faq/)
|
||||
* [History](https://www.rfc-editor.org/history/)
|
||||
* [About Us](https://www.rfc-editor.org/about/)
|
||||
* [Other Information](https://www.rfc-editor.org/other/)
|
||||
* [Publication Process](https://www.rfc-editor.org/pubprocess/)
|
||||
* [Publication Queue](/current_queue.php)
|
||||
* [Style Guide](https://www.rfc-editor.org/styleguide/)
|
||||
|
||||
[Advanced Search](/search/rfc_search.php)
|
||||
|
Binary file not shown.
Binary file not shown.
|
@ -1,92 +0,0 @@
|
|||
## Search RFCs
|
||||
|
||||
[Advanced Search](/search/rfc_search.php)
|
||||
|
||||
# [RFC Editor](https://www.rfc-editor.org/)
|
||||
|
||||
#
|
||||
|
||||
## [RFC 2046](https://www.rfc-editor.org/rfc/rfc2046.txt)
|
||||
|
||||
### Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types,
|
||||
November 1996
|
||||
|
||||
**File formats:** [![icon for text
|
||||
file](/rfcscripts/images/RFC_ICONS_Text_40x50.png)](https://www.rfc-
|
||||
editor.org/rfc/rfc2046.txt) [![icon for
|
||||
PDF](/rfcscripts/images/RFC_ICONS_PDF_2019_50x40.png)](https://www.rfc-
|
||||
editor.org/rfc/pdfrfc/rfc2046.txt.pdf) [![icon for
|
||||
HTML](/rfcscripts/images/RFC_ICONS_HTML_40x50.png)](https://www.rfc-
|
||||
editor.org/rfc/rfc2046.html) [![icon for inline
|
||||
errata](/rfcscripts/images/HTML_correction_40x50_2020.png)](https://www.rfc-
|
||||
editor.org/rfc/inline-errata/rfc2046.html)
|
||||
|
||||
**Status:** DRAFT STANDARD
|
||||
|
||||
**Obsoletes:** [RFC 1521](/info/rfc1521), [RFC 1522](/info/rfc1522), [RFC
|
||||
1590](/info/rfc1590)
|
||||
|
||||
**Updated by:** [RFC 2646](/info/rfc2646), [RFC 3798](/info/rfc3798), [RFC
|
||||
5147](/info/rfc5147), [RFC 6657](/info/rfc6657), [RFC 8098](/info/rfc8098)
|
||||
|
||||
**Authors:** N. Freed
|
||||
N. Borenstein
|
||||
|
||||
**Stream:** [IETF](https://www.ietf.org)
|
||||
|
||||
**Source:** [822ext](//datatracker.ietf.org/wg/822ext/about/)
|
||||
([app](//datatracker.ietf.org/wg/#app))
|
||||
|
||||
**Cite this RFC** : [TXT](/refs/ref2046.txt) |**
|
||||
[XML](https://bib.ietf.org/public/rfc/bibxml/reference.RFC.2046.xml) |
|
||||
[BibTeX](https://datatracker.ietf.org/doc/rfc2046/bibtex/)
|
||||
|
||||
**DOI** : 10.17487/RFC2046
|
||||
|
||||
**Discuss this RFC** : Send questions or comments to the mailing list
|
||||
[iesg@ietf.org](mailto:iesg@ietf.org?subject=Question regarding RFC 2046 )
|
||||
|
||||
**Other actions** : [View Errata](/errata/rfc2046) |** [Submit
|
||||
Errata](/errata.php#reportnew) |** [ Find IPR Disclosures from the
|
||||
IETF](https://datatracker.ietf.org/ipr/search/?draft=&rfc=2046&submit=rfc)
|
||||
|** [ View History of RFC 2046](https://datatracker.ietf.org/doc/rfc2046/)
|
||||
|
||||
* * *
|
||||
|
||||
## Abstract
|
||||
|
||||
This second document defines the general structure of the MIME media typing
|
||||
system and defines an initial set of media types. [STANDARDS-TRACK]
|
||||
|
||||
* * *
|
||||
|
||||
For the definition of **Status** , see [RFC 2026](/info/rfc2026).
|
||||
|
||||
For the definition of **Stream** , see [RFC 8729](/info/rfc8729).
|
||||
|
||||
* * *
|
||||
|
||||
|
||||
|
||||
|
||||
[IAB](//www.iab.org/) • [IANA](//www.iana.org/) • [IETF](//www.ietf.org) •
|
||||
[IRTF](//www.irtf.org) • [ISE](/about/independent) •
|
||||
[ISOC](//www.internetsociety.org) • [IETF Trust](//trustee.ietf.org/)
|
||||
[Reports](/report-summary) • [Privacy Statement](//www.ietf.org/privacy-
|
||||
statement/) • [Site Map](/sitemap) • [Contact Us](/contact)
|
||||
|
||||
[ ]()
|
||||
|
||||
* [Document Retrieval](https://www.rfc-editor.org/retrieve/)
|
||||
* [Errata](/errata.php)
|
||||
* [Frequently Asked Questions](https://www.rfc-editor.org/faq/)
|
||||
* [Future Format FAQ](https://www.rfc-editor.org/rse/format-faq/)
|
||||
* [History](https://www.rfc-editor.org/history/)
|
||||
* [About Us](https://www.rfc-editor.org/about/)
|
||||
* [Other Information](https://www.rfc-editor.org/other/)
|
||||
* [Publication Process](https://www.rfc-editor.org/pubprocess/)
|
||||
* [Publication Queue](/current_queue.php)
|
||||
* [Style Guide](https://www.rfc-editor.org/styleguide/)
|
||||
|
||||
[Advanced Search](/search/rfc_search.php)
|
||||
|
Binary file not shown.
Binary file not shown.
|
@ -1,87 +0,0 @@
|
|||
## Search RFCs
|
||||
|
||||
[Advanced Search](/search/rfc_search.php)
|
||||
|
||||
# [RFC Editor](https://www.rfc-editor.org/)
|
||||
|
||||
#
|
||||
|
||||
## [RFC 2369](https://www.rfc-editor.org/rfc/rfc2369.txt)
|
||||
|
||||
### The Use of URLs as Meta-Syntax for Core Mail List Commands and their
|
||||
Transport through Message Header Fields, July 1998
|
||||
|
||||
**File formats:** [![icon for text
|
||||
file](/rfcscripts/images/RFC_ICONS_Text_40x50.png)](https://www.rfc-
|
||||
editor.org/rfc/rfc2369.txt) [![icon for
|
||||
PDF](/rfcscripts/images/RFC_ICONS_PDF_2019_50x40.png)](https://www.rfc-
|
||||
editor.org/rfc/pdfrfc/rfc2369.txt.pdf) [![icon for
|
||||
HTML](/rfcscripts/images/RFC_ICONS_HTML_40x50.png)](https://www.rfc-
|
||||
editor.org/rfc/rfc2369.html)
|
||||
|
||||
**Status:** PROPOSED STANDARD
|
||||
|
||||
**Authors:** G. Neufeld
|
||||
J. Baer
|
||||
|
||||
**Stream:** [Legacy]
|
||||
|
||||
**Cite this RFC** : [TXT](/refs/ref2369.txt) |**
|
||||
[XML](https://bib.ietf.org/public/rfc/bibxml/reference.RFC.2369.xml) |
|
||||
[BibTeX](https://datatracker.ietf.org/doc/rfc2369/bibtex/)
|
||||
|
||||
**DOI** : 10.17487/RFC2369
|
||||
|
||||
**Discuss this RFC** : Send questions or comments to the mailing list
|
||||
[iesg@ietf.org](mailto:iesg@ietf.org?subject=Question regarding RFC 2369 )
|
||||
|
||||
**Other actions** : [Submit Errata](/errata.php#reportnew) |** [ Find IPR
|
||||
Disclosures from the
|
||||
IETF](https://datatracker.ietf.org/ipr/search/?draft=&rfc=2369&submit=rfc)
|
||||
|** [ View History of RFC 2369](https://datatracker.ietf.org/doc/rfc2369/)
|
||||
|
||||
* * *
|
||||
|
||||
## Abstract
|
||||
|
||||
The mailing list command specification header fields are a set of structured
|
||||
fields to be added to email messages sent by email distribution lists. By
|
||||
including these header fields, list servers can make it possible for mail
|
||||
clients to provide automated tools for users to perform list functions. This
|
||||
could take the form of a menu item, push button, or other user interface
|
||||
element. The intent is to simplify the user experience, providing a common
|
||||
interface to the often cryptic and varied mailing list manager commands.
|
||||
[STANDARDS-TRACK]
|
||||
|
||||
* * *
|
||||
|
||||
For the definition of **Status** , see [RFC 2026](/info/rfc2026).
|
||||
|
||||
For the definition of **Stream** , see [RFC 8729](/info/rfc8729).
|
||||
|
||||
* * *
|
||||
|
||||
|
||||
|
||||
|
||||
[IAB](//www.iab.org/) • [IANA](//www.iana.org/) • [IETF](//www.ietf.org) •
|
||||
[IRTF](//www.irtf.org) • [ISE](/about/independent) •
|
||||
[ISOC](//www.internetsociety.org) • [IETF Trust](//trustee.ietf.org/)
|
||||
[Reports](/report-summary) • [Privacy Statement](//www.ietf.org/privacy-
|
||||
statement/) • [Site Map](/sitemap) • [Contact Us](/contact)
|
||||
|
||||
[ ]()
|
||||
|
||||
* [Document Retrieval](https://www.rfc-editor.org/retrieve/)
|
||||
* [Errata](/errata.php)
|
||||
* [Frequently Asked Questions](https://www.rfc-editor.org/faq/)
|
||||
* [Future Format FAQ](https://www.rfc-editor.org/rse/format-faq/)
|
||||
* [History](https://www.rfc-editor.org/history/)
|
||||
* [About Us](https://www.rfc-editor.org/about/)
|
||||
* [Other Information](https://www.rfc-editor.org/other/)
|
||||
* [Publication Process](https://www.rfc-editor.org/pubprocess/)
|
||||
* [Publication Queue](/current_queue.php)
|
||||
* [Style Guide](https://www.rfc-editor.org/styleguide/)
|
||||
|
||||
[Advanced Search](/search/rfc_search.php)
|
||||
|
Binary file not shown.
Binary file not shown.
|
@ -1,90 +0,0 @@
|
|||
## Search RFCs
|
||||
|
||||
[Advanced Search](/search/rfc_search.php)
|
||||
|
||||
# [RFC Editor](https://www.rfc-editor.org/)
|
||||
|
||||
#
|
||||
|
||||
## [RFC 2919](https://www.rfc-editor.org/rfc/rfc2919.txt)
|
||||
|
||||
### List-Id: A Structured Field and Namespace for the Identification of
|
||||
Mailing Lists, March 2001
|
||||
|
||||
**File formats:** [![icon for text
|
||||
file](/rfcscripts/images/RFC_ICONS_Text_40x50.png)](https://www.rfc-
|
||||
editor.org/rfc/rfc2919.txt) [![icon for
|
||||
PDF](/rfcscripts/images/RFC_ICONS_PDF_2019_50x40.png)](https://www.rfc-
|
||||
editor.org/rfc/pdfrfc/rfc2919.txt.pdf) [![icon for
|
||||
HTML](/rfcscripts/images/RFC_ICONS_HTML_40x50.png)](https://www.rfc-
|
||||
editor.org/rfc/rfc2919.html) [![icon for inline
|
||||
errata](/rfcscripts/images/HTML_correction_40x50_2020.png)](https://www.rfc-
|
||||
editor.org/rfc/inline-errata/rfc2919.html)
|
||||
|
||||
**Status:** PROPOSED STANDARD
|
||||
|
||||
**Authors:** R. Chandhok
|
||||
G. Wenger
|
||||
|
||||
**Stream:** [IETF](https://www.ietf.org)
|
||||
|
||||
**Source:** [NON WORKING GROUP](https://www.ietf.org/blog/guidance-area-
|
||||
director-sponsoring-documents)
|
||||
|
||||
**Cite this RFC** : [TXT](/refs/ref2919.txt) |**
|
||||
[XML](https://bib.ietf.org/public/rfc/bibxml/reference.RFC.2919.xml) |
|
||||
[BibTeX](https://datatracker.ietf.org/doc/rfc2919/bibtex/)
|
||||
|
||||
**DOI** : 10.17487/RFC2919
|
||||
|
||||
**Discuss this RFC** : Send questions or comments to the mailing list
|
||||
[iesg@ietf.org](mailto:iesg@ietf.org?subject=Question regarding RFC 2919 )
|
||||
|
||||
**Other actions** : [View Errata](/errata/rfc2919) |** [Submit
|
||||
Errata](/errata.php#reportnew) |** [ Find IPR Disclosures from the
|
||||
IETF](https://datatracker.ietf.org/ipr/search/?draft=&rfc=2919&submit=rfc)
|
||||
|** [ View History of RFC 2919](https://datatracker.ietf.org/doc/rfc2919/)
|
||||
|
||||
* * *
|
||||
|
||||
## Abstract
|
||||
|
||||
Software that handles electronic mailing list messages (servers and user
|
||||
agents) needs a way to reliably identify messages that belong to a particular
|
||||
mailing list. With the advent of list management headers, it has become even
|
||||
more important to provide a unique identifier for a mailing list regardless of
|
||||
the particular host that serves as the list processor at any given time.
|
||||
[STANDARDS-TRACK]
|
||||
|
||||
* * *
|
||||
|
||||
For the definition of **Status** , see [RFC 2026](/info/rfc2026).
|
||||
|
||||
For the definition of **Stream** , see [RFC 8729](/info/rfc8729).
|
||||
|
||||
* * *
|
||||
|
||||
|
||||
|
||||
|
||||
[IAB](//www.iab.org/) • [IANA](//www.iana.org/) • [IETF](//www.ietf.org) •
|
||||
[IRTF](//www.irtf.org) • [ISE](/about/independent) •
|
||||
[ISOC](//www.internetsociety.org) • [IETF Trust](//trustee.ietf.org/)
|
||||
[Reports](/report-summary) • [Privacy Statement](//www.ietf.org/privacy-
|
||||
statement/) • [Site Map](/sitemap) • [Contact Us](/contact)
|
||||
|
||||
[ ]()
|
||||
|
||||
* [Document Retrieval](https://www.rfc-editor.org/retrieve/)
|
||||
* [Errata](/errata.php)
|
||||
* [Frequently Asked Questions](https://www.rfc-editor.org/faq/)
|
||||
* [Future Format FAQ](https://www.rfc-editor.org/rse/format-faq/)
|
||||
* [History](https://www.rfc-editor.org/history/)
|
||||
* [About Us](https://www.rfc-editor.org/about/)
|
||||
* [Other Information](https://www.rfc-editor.org/other/)
|
||||
* [Publication Process](https://www.rfc-editor.org/pubprocess/)
|
||||
* [Publication Queue](/current_queue.php)
|
||||
* [Style Guide](https://www.rfc-editor.org/styleguide/)
|
||||
|
||||
[Advanced Search](/search/rfc_search.php)
|
||||
|
Binary file not shown.
Binary file not shown.
|
@ -1,77 +0,0 @@
|
|||
# Ideas, plans, thoughts on `mailpot`.
|
||||
|
||||
It'd be better if this stuff wasn't on an issue tracker like gitea's or
|
||||
github's but committed in the repository.
|
||||
|
||||
Discussion about these notes can take place in the mailing list,
|
||||
[`<mailpot-general@meli.delivery>`](https://lists.meli.delivery/list/mailpot-general/).
|
||||
|
||||
In no particular order:
|
||||
|
||||
**Table of contents**:
|
||||
|
||||
* [Possible Postfix integrations](#possible-postfix-integrations)
|
||||
* [Setup docker container network with postfix for testing](#setup-docker-container-network-with-postfix-for-testing)
|
||||
* [Add NNTP gateways](#add-nntp-gateways)
|
||||
* [Add MIME type filter for list owners](#add-mime-type-filter-for-list-owners)
|
||||
* [Add `convert_html_to_plaintext` filter](#add-convert_html_to_plaintext-filter)
|
||||
* [Use mdoc instead of roff for manpages](#use-mdoc-instead-of-roff-for-manpages)
|
||||
* [Add shell completions with `clap`](#add-shell-completions-with-clap)
|
||||
* [Make complex database logic and/or complex migrations with user defined functions](#make-complex-database-logic-andor-complex-migrations-with-user-defined-functions)
|
||||
* [Implement dtolnay's mailing set concept](#implement-dtolnays-mailing-set-concept)
|
||||
|
||||
## Possible Postfix integrations
|
||||
|
||||
- local delivery with `postdrop(1)` instead of SMTP
|
||||
- log with `postlog(1)`
|
||||
- sqlite maps <https://www.postfix.org/SQLITE_README.html>
|
||||
|
||||
## Setup docker container network with postfix for testing
|
||||
|
||||
Beyond integration tests, we need a real-world testcase: a bunch of user postfixes talking to a mailing list postfix.
|
||||
This can be done with a docker setup.
|
||||
A simple debian slim image can be used for this.
|
||||
Reference for postfix on docker: <https://www.frakkingsweet.com/postfix-in-a-container/>.
|
||||
It'd be great if we could use a Rust based solution as well, with something like <https://github.com/fussybeaver/bollard>.
|
||||
|
||||
## Add NNTP gateways
|
||||
|
||||
TODO
|
||||
|
||||
## Add MIME type filter for list owners
|
||||
|
||||
TODO
|
||||
|
||||
## Add `convert_html_to_plaintext` filter
|
||||
|
||||
TODO
|
||||
|
||||
## Use mdoc instead of roff for manpages
|
||||
|
||||
[`mdoc` reference](https://man.openbsd.org/mdoc.7)
|
||||
|
||||
Progress:
|
||||
|
||||
- Got ownership of `mdoc` on crates.io.
|
||||
- Forked `roff` crate to use as a basis: <https://github.com/epilys/mdoc>
|
||||
|
||||
## Add shell completions with `clap`
|
||||
|
||||
Probably with <https://docs.rs/clap_complete/latest/clap_complete/>
|
||||
|
||||
## Make complex database logic and/or complex migrations with user defined functions
|
||||
|
||||
Useful projects:
|
||||
|
||||
- <https://github.com/facebookincubator/CG-SQL/tree/main>
|
||||
- <https://github.com/epilys/vfsstat.rs>
|
||||
|
||||
## Implement dtolnay's mailing set concept
|
||||
|
||||
See <https://github.com/dtolnay/mailingset/tree/master>
|
||||
|
||||
> A mailing list server that treates mailing lists as sets and allows mail to
|
||||
> be sent to the result of set-algebraic expressions on those sets. The union,
|
||||
> intersection, and difference operators are supported. Sending mail to a set
|
||||
> operation involves specifying a set expression in the local part of the
|
||||
> recipient email address.
|
|
@ -1,25 +0,0 @@
|
|||
[package]
|
||||
name = "mailpot-archives"
|
||||
version = "0.1.1"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2021"
|
||||
license = "LICENSE"
|
||||
readme = "README.md"
|
||||
description = "mailing list manager"
|
||||
repository = "https://github.com/meli/mailpot"
|
||||
keywords = ["mail", "mailing-lists"]
|
||||
categories = ["email"]
|
||||
default-run = "mpot-archives"
|
||||
|
||||
[[bin]]
|
||||
name = "mpot-archives"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "^0.4" }
|
||||
lazy_static = "^1.4"
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
minijinja = { version = "0.31.0", features = ["source", ] }
|
||||
percent-encoding = { version = "^2.1", optional = true }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
||||
serde_json = "^1"
|
|
@ -1 +0,0 @@
|
|||
../rustfmt.toml
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod cal;
|
||||
pub mod utils;
|
|
@ -1,257 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{fs::OpenOptions, io::Write};
|
||||
|
||||
use mailpot::*;
|
||||
use mailpot_archives::utils::*;
|
||||
use minijinja::value::Value;
|
||||
|
||||
fn run_app() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
let Some(config_path) = args.get(1) else {
|
||||
return Err("Expected configuration file path as first argument.".into());
|
||||
};
|
||||
let Some(output_path) = args.get(2) else {
|
||||
return Err("Expected output dir path as second argument.".into());
|
||||
};
|
||||
let root_url_prefix = args.get(3).cloned().unwrap_or_default();
|
||||
|
||||
let output_path = std::path::Path::new(&output_path);
|
||||
if output_path.exists() && !output_path.is_dir() {
|
||||
return Err("Output path is not a directory.".into());
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(output_path.join("lists"))?;
|
||||
std::fs::create_dir_all(output_path.join("list"))?;
|
||||
let conf = Configuration::from_file(config_path)
|
||||
.map_err(|err| format!("Could not load config {config_path}: {err}"))?;
|
||||
|
||||
let db = Connection::open_db(conf).map_err(|err| format!("Couldn't open db: {err}"))?;
|
||||
let lists_values = db.lists()?;
|
||||
{
|
||||
//index.html
|
||||
|
||||
let lists = lists_values
|
||||
.iter()
|
||||
.map(|list| {
|
||||
let months = db.months(list.pk).unwrap();
|
||||
let posts = db.list_posts(list.pk, None).unwrap();
|
||||
minijinja::context! {
|
||||
title => &list.name,
|
||||
posts => &posts,
|
||||
months => &months,
|
||||
body => &list.description.as_deref().unwrap_or_default(),
|
||||
root_prefix => &root_url_prefix,
|
||||
list => Value::from_object(MailingList::from(list.clone())),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(output_path.join("index.html"))?;
|
||||
let crumbs = vec![Crumb {
|
||||
label: "Lists".into(),
|
||||
url: format!("{root_url_prefix}/").into(),
|
||||
}];
|
||||
|
||||
let context = minijinja::context! {
|
||||
title => "mailing list archive",
|
||||
description => "",
|
||||
lists => &lists,
|
||||
root_prefix => &root_url_prefix,
|
||||
crumbs => crumbs,
|
||||
};
|
||||
file.write_all(
|
||||
TEMPLATES
|
||||
.get_template("lists.html")?
|
||||
.render(context)?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut lists_path = output_path.to_path_buf();
|
||||
|
||||
for list in &lists_values {
|
||||
lists_path.push("lists");
|
||||
lists_path.push(list.pk.to_string());
|
||||
std::fs::create_dir_all(&lists_path)?;
|
||||
lists_path.push("index.html");
|
||||
|
||||
let list = db.list(list.pk)?.unwrap();
|
||||
let post_policy = db.list_post_policy(list.pk)?;
|
||||
let months = db.months(list.pk)?;
|
||||
let posts = db.list_posts(list.pk, None)?;
|
||||
let mut hist = months
|
||||
.iter()
|
||||
.map(|m| (m.to_string(), [0usize; 31]))
|
||||
.collect::<std::collections::HashMap<String, [usize; 31]>>();
|
||||
let posts_ctx = posts
|
||||
.iter()
|
||||
.map(|post| {
|
||||
//2019-07-14T14:21:02
|
||||
if let Some(day) = post.datetime.get(8..10).and_then(|d| d.parse::<u64>().ok()) {
|
||||
hist.get_mut(&post.month_year).unwrap()[day.saturating_sub(1) as usize] += 1;
|
||||
}
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.expect("Could not parse mail");
|
||||
let mut msg_id = &post.message_id[1..];
|
||||
msg_id = &msg_id[..msg_id.len().saturating_sub(1)];
|
||||
let subject = envelope.subject();
|
||||
let mut subject_ref = subject.trim();
|
||||
if subject_ref.starts_with('[')
|
||||
&& subject_ref[1..].starts_with(&list.id)
|
||||
&& subject_ref[1 + list.id.len()..].starts_with(']')
|
||||
{
|
||||
subject_ref = subject_ref[2 + list.id.len()..].trim();
|
||||
}
|
||||
minijinja::context! {
|
||||
pk => post.pk,
|
||||
list => post.list,
|
||||
subject => subject_ref,
|
||||
address=> post.address,
|
||||
message_id => msg_id,
|
||||
message => post.message,
|
||||
timestamp => post.timestamp,
|
||||
datetime => post.datetime,
|
||||
root_prefix => &root_url_prefix,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let crumbs = vec![
|
||||
Crumb {
|
||||
label: "Lists".into(),
|
||||
url: format!("{root_url_prefix}/").into(),
|
||||
},
|
||||
Crumb {
|
||||
label: list.name.clone().into(),
|
||||
url: format!("{root_url_prefix}/lists/{}/", list.pk).into(),
|
||||
},
|
||||
];
|
||||
let context = minijinja::context! {
|
||||
title=> &list.name,
|
||||
description=> &list.description,
|
||||
post_policy=> &post_policy,
|
||||
preamble => true,
|
||||
months=> &months,
|
||||
hists => &hist,
|
||||
posts=> posts_ctx,
|
||||
body=>&list.description.clone().unwrap_or_default(),
|
||||
root_prefix => &root_url_prefix,
|
||||
list => Value::from_object(MailingList::from(list.clone())),
|
||||
crumbs => crumbs,
|
||||
};
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&lists_path)
|
||||
.map_err(|err| format!("could not open {lists_path:?}: {err}"))?;
|
||||
file.write_all(
|
||||
TEMPLATES
|
||||
.get_template("list.html")?
|
||||
.render(context)?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
lists_path.pop();
|
||||
lists_path.pop();
|
||||
lists_path.pop();
|
||||
lists_path.push("list");
|
||||
lists_path.push(list.pk.to_string());
|
||||
std::fs::create_dir_all(&lists_path)?;
|
||||
|
||||
for post in posts {
|
||||
let mut msg_id = &post.message_id[1..];
|
||||
msg_id = &msg_id[..msg_id.len().saturating_sub(1)];
|
||||
lists_path.push(format!("{msg_id}.html"));
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.map_err(|err| format!("Could not parse mail {}: {err}", post.message_id))?;
|
||||
let body = envelope.body_bytes(post.message.as_slice());
|
||||
let body_text = body.text();
|
||||
let subject = envelope.subject();
|
||||
let mut subject_ref = subject.trim();
|
||||
if subject_ref.starts_with('[')
|
||||
&& subject_ref[1..].starts_with(&list.id)
|
||||
&& subject_ref[1 + list.id.len()..].starts_with(']')
|
||||
{
|
||||
subject_ref = subject_ref[2 + list.id.len()..].trim();
|
||||
}
|
||||
let mut message_id = &post.message_id[1..];
|
||||
message_id = &message_id[..message_id.len().saturating_sub(1)];
|
||||
let crumbs = vec![
|
||||
Crumb {
|
||||
label: "Lists".into(),
|
||||
url: format!("{root_url_prefix}/").into(),
|
||||
},
|
||||
Crumb {
|
||||
label: list.name.clone().into(),
|
||||
url: format!("{root_url_prefix}/lists/{}/", list.pk).into(),
|
||||
},
|
||||
Crumb {
|
||||
label: subject_ref.to_string().into(),
|
||||
url: format!("{root_url_prefix}/lists/{}/{message_id}.html/", list.pk).into(),
|
||||
},
|
||||
];
|
||||
let context = minijinja::context! {
|
||||
title => &list.name,
|
||||
list => &list,
|
||||
post => &post,
|
||||
posts => &posts_ctx,
|
||||
body => &body_text,
|
||||
from => &envelope.field_from_to_string(),
|
||||
date => &envelope.date_as_str(),
|
||||
to => &envelope.field_to_to_string(),
|
||||
subject => &envelope.subject(),
|
||||
trimmed_subject => subject_ref,
|
||||
in_reply_to => &envelope.in_reply_to_display().map(|r| r.to_string().as_str().strip_carets().to_string()),
|
||||
references => &envelope .references() .into_iter() .map(|m| m.to_string().as_str().strip_carets().to_string()) .collect::<Vec<String>>(),
|
||||
root_prefix => &root_url_prefix,
|
||||
crumbs => crumbs,
|
||||
};
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&lists_path)
|
||||
.map_err(|err| format!("could not open {lists_path:?}: {err}"))?;
|
||||
file.write_all(
|
||||
TEMPLATES
|
||||
.get_template("post.html")?
|
||||
.render(context)?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
lists_path.pop();
|
||||
}
|
||||
lists_path.pop();
|
||||
lists_path.pop();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> std::result::Result<(), i64> {
|
||||
if let Err(err) = run_app() {
|
||||
eprintln!("{err}");
|
||||
return Err(-1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use chrono::{Datelike, Month};
|
||||
use mailpot::{models::DbVal, *};
|
||||
use minijinja::{
|
||||
value::{Object, Value},
|
||||
Environment, Error, Source, State,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TEMPLATES: Environment<'static> = {
|
||||
let mut env = Environment::new();
|
||||
env.add_function("calendarize", calendarize);
|
||||
env.set_source(Source::from_path("src/templates/"));
|
||||
|
||||
env
|
||||
};
|
||||
}
|
||||
|
||||
pub trait StripCarets {
|
||||
fn strip_carets(&self) -> &str;
|
||||
}
|
||||
|
||||
impl StripCarets for &str {
|
||||
fn strip_carets(&self) -> &str {
|
||||
let mut self_ref = self.trim();
|
||||
if self_ref.starts_with('<') && self_ref.ends_with('>') {
|
||||
self_ref = &self_ref[1..self_ref.len().saturating_sub(1)];
|
||||
}
|
||||
self_ref
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MailingList {
|
||||
pub pk: i64,
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub address: String,
|
||||
pub description: Option<String>,
|
||||
pub topics: Vec<String>,
|
||||
pub archive_url: Option<String>,
|
||||
pub inner: DbVal<mailpot::models::MailingList>,
|
||||
}
|
||||
|
||||
impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||
fn from(val: DbVal<mailpot::models::MailingList>) -> Self {
|
||||
let DbVal(
|
||||
mailpot::models::MailingList {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
topics,
|
||||
archive_url,
|
||||
},
|
||||
_,
|
||||
) = val.clone();
|
||||
|
||||
Self {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
topics,
|
||||
archive_url,
|
||||
inner: val,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MailingList {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
self.id.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for MailingList {
|
||||
fn kind(&self) -> minijinja::value::ObjectKind {
|
||||
minijinja::value::ObjectKind::Struct(self)
|
||||
}
|
||||
|
||||
fn call_method(
|
||||
&self,
|
||||
_state: &State,
|
||||
name: &str,
|
||||
_args: &[Value],
|
||||
) -> std::result::Result<Value, Error> {
|
||||
match name {
|
||||
"subscription_mailto" => {
|
||||
Ok(Value::from_serializable(&self.inner.subscription_mailto()))
|
||||
}
|
||||
"unsubscription_mailto" => Ok(Value::from_serializable(
|
||||
&self.inner.unsubscription_mailto(),
|
||||
)),
|
||||
_ => Err(Error::new(
|
||||
minijinja::ErrorKind::UnknownMethod,
|
||||
format!("aaaobject has no method named {name}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl minijinja::value::StructObject for MailingList {
|
||||
fn get_field(&self, name: &str) -> Option<Value> {
|
||||
match name {
|
||||
"pk" => Some(Value::from_serializable(&self.pk)),
|
||||
"name" => Some(Value::from_serializable(&self.name)),
|
||||
"id" => Some(Value::from_serializable(&self.id)),
|
||||
"address" => Some(Value::from_serializable(&self.address)),
|
||||
"description" => Some(Value::from_serializable(&self.description)),
|
||||
"topics" => Some(Value::from_serializable(&self.topics)),
|
||||
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
||||
Some(
|
||||
&[
|
||||
"pk",
|
||||
"name",
|
||||
"id",
|
||||
"address",
|
||||
"description",
|
||||
"topics",
|
||||
"archive_url",
|
||||
][..],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calendarize(_state: &State, args: Value, hists: Value) -> std::result::Result<Value, Error> {
|
||||
macro_rules! month {
|
||||
($int:expr) => {{
|
||||
let int = $int;
|
||||
match int {
|
||||
1 => Month::January.name(),
|
||||
2 => Month::February.name(),
|
||||
3 => Month::March.name(),
|
||||
4 => Month::April.name(),
|
||||
5 => Month::May.name(),
|
||||
6 => Month::June.name(),
|
||||
7 => Month::July.name(),
|
||||
8 => Month::August.name(),
|
||||
9 => Month::September.name(),
|
||||
10 => Month::October.name(),
|
||||
11 => Month::November.name(),
|
||||
12 => Month::December.name(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}};
|
||||
}
|
||||
let month = args.as_str().unwrap();
|
||||
let hist = hists
|
||||
.get_item(&Value::from(month))?
|
||||
.as_seq()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| usize::try_from(v).unwrap())
|
||||
.collect::<Vec<usize>>();
|
||||
let sum: usize = hists
|
||||
.get_item(&Value::from(month))?
|
||||
.as_seq()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| usize::try_from(v).unwrap())
|
||||
.sum();
|
||||
let date = chrono::NaiveDate::parse_from_str(&format!("{}-01", month), "%F").unwrap();
|
||||
// Week = [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
|
||||
Ok(minijinja::context! {
|
||||
month_name => month!(date.month()),
|
||||
month => month,
|
||||
month_int => date.month() as usize,
|
||||
year => date.year(),
|
||||
weeks => crate::cal::calendarize_with_offset(date, 1),
|
||||
hist => hist,
|
||||
sum => sum,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Crumb {
|
||||
pub label: Cow<'static, str>,
|
||||
pub url: Cow<'static, str>,
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
[package]
|
||||
name = "mailpot-cli"
|
||||
version = "0.1.1"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2021"
|
||||
license = "LICENSE"
|
||||
readme = "README.md"
|
||||
description = "mailing list manager"
|
||||
repository = "https://github.com/meli/mailpot"
|
||||
keywords = ["mail", "mailing-lists"]
|
||||
categories = ["email"]
|
||||
default-run = "mpot"
|
||||
|
||||
[[bin]]
|
||||
name = "mpot"
|
||||
path = "src/main.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { version = "0.21" }
|
||||
clap = { version = "^4.2", default-features = false, features = ["std", "derive", "cargo", "unicode", "help", "usage", "error-context", "suggestions"] }
|
||||
log = "0.4"
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
||||
serde_json = "^1"
|
||||
stderrlog = { version = "^0.6" }
|
||||
ureq = { version = "2.6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2"
|
||||
mailpot-tests = { version = "^0.1", path = "../mailpot-tests" }
|
||||
predicates = "3"
|
||||
tempfile = { version = "3.9" }
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "^4.2", default-features = false, features = ["std", "derive", "cargo", "unicode", "wrap_help", "help", "usage", "error-context", "suggestions"] }
|
||||
clap_mangen = "0.2.10"
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
stderrlog = { version = "^0.6" }
|
|
@ -1,524 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::RandomState, HashSet, VecDeque},
|
||||
hash::{BuildHasher, Hasher},
|
||||
io::Write,
|
||||
};
|
||||
|
||||
use clap::{ArgAction, CommandFactory};
|
||||
use clap_mangen::{roff, Man};
|
||||
use roff::{bold, italic, roman, Inline, Roff};
|
||||
|
||||
include!("src/args.rs");
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
println!("cargo:rerun-if-changed=./src/lib.rs");
|
||||
println!("cargo:rerun-if-changed=./build.rs");
|
||||
std::env::set_current_dir("..").expect("could not chdir('..')");
|
||||
|
||||
let out_dir = PathBuf::from("./docs/");
|
||||
|
||||
let cmd = Opt::command();
|
||||
|
||||
let man = Man::new(cmd.clone()).title("mpot");
|
||||
let mut buffer: Vec<u8> = Default::default();
|
||||
man.render_title(&mut buffer)?;
|
||||
man.render_name_section(&mut buffer)?;
|
||||
man.render_synopsis_section(&mut buffer)?;
|
||||
man.render_description_section(&mut buffer)?;
|
||||
|
||||
let mut roff = Roff::default();
|
||||
options(&mut roff, &cmd);
|
||||
roff.to_writer(&mut buffer)?;
|
||||
|
||||
render_quick_start_section(&mut buffer)?;
|
||||
render_subcommands_section(&mut buffer)?;
|
||||
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
let mut stack = VecDeque::new();
|
||||
let mut order = VecDeque::new();
|
||||
stack.push_back(vec![&cmd]);
|
||||
let s = RandomState::new();
|
||||
|
||||
'stack: while let Some(cmds) = stack.pop_front() {
|
||||
for sub in cmds.last().unwrap().get_subcommands() {
|
||||
let mut hasher = s.build_hasher();
|
||||
for c in cmds.iter() {
|
||||
hasher.write(c.get_name().as_bytes());
|
||||
}
|
||||
hasher.write(sub.get_name().as_bytes());
|
||||
if visited.insert(hasher.finish()) {
|
||||
let mut sub_cmds = cmds.clone();
|
||||
sub_cmds.push(sub);
|
||||
order.push_back(sub_cmds.clone());
|
||||
stack.push_front(cmds);
|
||||
stack.push_front(sub_cmds);
|
||||
continue 'stack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(mut subs) = order.pop_front() {
|
||||
let sub = subs.pop().unwrap();
|
||||
render_subcommand(&subs, sub, &mut buffer)?;
|
||||
}
|
||||
|
||||
man.render_authors_section(&mut buffer)?;
|
||||
|
||||
std::fs::write(out_dir.join("mpot.1"), buffer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_quick_start_section(w: &mut dyn Write) -> Result<(), std::io::Error> {
|
||||
let mut roff = Roff::default();
|
||||
let heading = "QUICK START";
|
||||
roff.control("SH", [heading]);
|
||||
let tutorial = r#"mailpot saves its data in a sqlite3 file. To define the location of the sqlite3 file we need a configuration file, which can be generated with:
|
||||
|
||||
mpot sample-config > conf.toml
|
||||
|
||||
Mailing lists can now be created:
|
||||
|
||||
mpot -c conf.toml create-list --name "my first list" --id mylist --address mylist@example.com
|
||||
|
||||
You can list all the mailing lists with:
|
||||
|
||||
mpot -c conf.toml list-lists
|
||||
|
||||
You should add yourself as the list owner:
|
||||
|
||||
mpot -c conf.toml list mylist add-list-owner --address myself@example.com --name "Nemo"
|
||||
|
||||
And also enable posting and subscriptions by setting list policies:
|
||||
|
||||
mpot -c conf.toml list mylist add-policy --subscriber-only
|
||||
|
||||
mpot -c conf.toml list mylist add-subscribe-policy --request --send-confirmation
|
||||
|
||||
To post on a mailing list or submit a list request, pipe a raw e-mail into STDIN:
|
||||
|
||||
mpot -c conf.toml post
|
||||
|
||||
You can configure your mail server to redirect e-mails addressed to your mailing lists to this command.
|
||||
|
||||
For postfix, you can automatically generate this configuration with:
|
||||
|
||||
mpot -c conf.toml print-postfix-config --user myself --binary-path /path/to/mpot
|
||||
|
||||
This will print the following:
|
||||
|
||||
- content of `transport_maps` and `local_recipient_maps`
|
||||
|
||||
The output must be saved in a plain text file.
|
||||
Map output should be added to transport_maps and local_recipient_maps parameters in postfix's main.cf.
|
||||
To make postfix be able to read them, the postmap application must be executed with the
|
||||
path to the map file as its sole argument.
|
||||
|
||||
postmap /path/to/mylist_maps
|
||||
|
||||
postmap is usually distributed along with the other postfix binaries.
|
||||
|
||||
- `master.cf` service entry
|
||||
The output must be entered in the master.cf file.
|
||||
See <https://www.postfix.org/master.5.html>.
|
||||
|
||||
"#;
|
||||
for line in tutorial.lines() {
|
||||
roff.text([roman(line.trim())]);
|
||||
}
|
||||
roff.to_writer(w)
|
||||
}
|
||||
fn render_subcommands_section(w: &mut dyn Write) -> Result<(), std::io::Error> {
|
||||
let mut roff = Roff::default();
|
||||
let heading = "SUBCOMMANDS";
|
||||
roff.control("SH", [heading]);
|
||||
roff.to_writer(w)
|
||||
}
|
||||
|
||||
fn render_subcommand(
|
||||
parents: &[&clap::Command],
|
||||
sub: &clap::Command,
|
||||
w: &mut dyn Write,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut roff = Roff::default();
|
||||
_render_subcommand_full(parents, sub, &mut roff);
|
||||
options(&mut roff, sub);
|
||||
roff.to_writer(w)
|
||||
}
|
||||
|
||||
fn _render_subcommand_full(parents: &[&clap::Command], sub: &clap::Command, roff: &mut Roff) {
|
||||
roff.control("\\fB", []);
|
||||
roff.control(
|
||||
"SS",
|
||||
parents
|
||||
.iter()
|
||||
.map(|cmd| cmd.get_name())
|
||||
.chain(std::iter::once(sub.get_name()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
roff.control("\\fR", []);
|
||||
roff.text([Inline::LineBreak]);
|
||||
|
||||
synopsis(roff, parents, sub);
|
||||
roff.text([Inline::LineBreak]);
|
||||
|
||||
if let Some(about) = sub.get_about().or_else(|| sub.get_long_about()) {
|
||||
let about = about.to_string();
|
||||
let mut iter = about.lines();
|
||||
let last = iter.nth_back(0);
|
||||
for line in iter {
|
||||
roff.text([roman(line.trim())]);
|
||||
}
|
||||
if let Some(line) = last {
|
||||
roff.text([roman(format!("{}.", line.trim()))]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn synopsis(roff: &mut Roff, parents: &[&clap::Command], sub: &clap::Command) {
|
||||
let mut line = parents
|
||||
.iter()
|
||||
.flat_map(|cmd| vec![roman(cmd.get_name()), roman(" ")].into_iter())
|
||||
.chain(std::iter::once(roman(sub.get_name())))
|
||||
.chain(std::iter::once(roman(" ")))
|
||||
.collect::<Vec<_>>();
|
||||
let arguments = sub
|
||||
.get_arguments()
|
||||
.filter(|i| !i.is_hide_set())
|
||||
.collect::<Vec<_>>();
|
||||
if arguments.is_empty() && sub.get_positionals().count() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
roff.text([Inline::LineBreak]);
|
||||
|
||||
for opt in arguments {
|
||||
match (opt.get_short(), opt.get_long()) {
|
||||
(Some(short), Some(long)) => {
|
||||
let (lhs, rhs) = option_markers(opt);
|
||||
line.push(roman(lhs));
|
||||
line.push(roman(format!("-{short}")));
|
||||
if let Some(value) = opt.get_value_names() {
|
||||
line.push(roman(" "));
|
||||
line.push(italic(value.join(" ")));
|
||||
}
|
||||
|
||||
line.push(roman("|"));
|
||||
line.push(roman(format!("--{long}",)));
|
||||
line.push(roman(rhs));
|
||||
}
|
||||
(Some(short), None) => {
|
||||
let (lhs, rhs) = option_markers_single(opt);
|
||||
line.push(roman(lhs));
|
||||
line.push(roman(format!("-{short}")));
|
||||
if let Some(value) = opt.get_value_names() {
|
||||
line.push(roman(" "));
|
||||
line.push(italic(value.join(" ")));
|
||||
}
|
||||
line.push(roman(rhs));
|
||||
}
|
||||
(None, Some(long)) => {
|
||||
let (lhs, rhs) = option_markers_single(opt);
|
||||
line.push(roman(lhs));
|
||||
line.push(roman(format!("--{long}")));
|
||||
if let Some(value) = opt.get_value_names() {
|
||||
line.push(roman(" "));
|
||||
line.push(italic(value.join(" ")));
|
||||
}
|
||||
line.push(roman(rhs));
|
||||
}
|
||||
(None, None) => continue,
|
||||
};
|
||||
|
||||
if matches!(opt.get_action(), ArgAction::Count) {
|
||||
line.push(roman("..."))
|
||||
}
|
||||
line.push(roman(" "));
|
||||
}
|
||||
|
||||
for arg in sub.get_positionals() {
|
||||
let (lhs, rhs) = option_markers_single(arg);
|
||||
line.push(roman(lhs));
|
||||
if let Some(value) = arg.get_value_names() {
|
||||
line.push(italic(value.join(" ")));
|
||||
} else {
|
||||
line.push(italic(arg.get_id().as_str()));
|
||||
}
|
||||
line.push(roman(rhs));
|
||||
line.push(roman(" "));
|
||||
}
|
||||
|
||||
roff.text(line);
|
||||
}
|
||||
|
||||
fn options(roff: &mut Roff, cmd: &clap::Command) {
|
||||
let items: Vec<_> = cmd.get_arguments().filter(|i| !i.is_hide_set()).collect();
|
||||
|
||||
for pos in items.iter().filter(|a| a.is_positional()) {
|
||||
let mut header = vec![];
|
||||
let (lhs, rhs) = option_markers_single(pos);
|
||||
header.push(roman(lhs));
|
||||
if let Some(value) = pos.get_value_names() {
|
||||
header.push(italic(value.join(" ")));
|
||||
} else {
|
||||
header.push(italic(pos.get_id().as_str()));
|
||||
};
|
||||
header.push(roman(rhs));
|
||||
|
||||
if let Some(defs) = option_default_values(pos) {
|
||||
header.push(roman(format!(" {defs}")));
|
||||
}
|
||||
|
||||
let mut body = vec![];
|
||||
let mut arg_help_written = false;
|
||||
if let Some(help) = option_help(pos) {
|
||||
arg_help_written = true;
|
||||
let mut help = help.to_string();
|
||||
if !help.ends_with('.') {
|
||||
help.push('.');
|
||||
}
|
||||
body.push(roman(help));
|
||||
}
|
||||
|
||||
roff.control("TP", []);
|
||||
roff.text(header);
|
||||
roff.text(body);
|
||||
|
||||
if let Some(env) = option_environment(pos) {
|
||||
roff.control("RS", []);
|
||||
roff.text(env);
|
||||
roff.control("RE", []);
|
||||
}
|
||||
// If possible options are available
|
||||
if let Some((possible_values_text, with_help)) = get_possible_values(pos) {
|
||||
if arg_help_written {
|
||||
// It looks nice to have a separation between the help and the values
|
||||
roff.text([Inline::LineBreak]);
|
||||
}
|
||||
if with_help {
|
||||
roff.text([Inline::LineBreak, italic("Possible values:")]);
|
||||
|
||||
// Need to indent twice to get it to look right, because .TP heading indents,
|
||||
// but that indent doesn't Carry over to the .IP for the
|
||||
// bullets. The standard shift size is 7 for terminal devices
|
||||
roff.control("RS", ["14"]);
|
||||
for line in possible_values_text {
|
||||
roff.control("IP", ["\\(bu", "2"]);
|
||||
roff.text([roman(line)]);
|
||||
}
|
||||
roff.control("RE", []);
|
||||
} else {
|
||||
let possible_value_text: Vec<Inline> = vec![
|
||||
Inline::LineBreak,
|
||||
roman("["),
|
||||
italic("possible values: "),
|
||||
roman(possible_values_text.join(", ")),
|
||||
roman("]"),
|
||||
];
|
||||
roff.text(possible_value_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for opt in items.iter().filter(|a| !a.is_positional()) {
|
||||
let mut header = match (opt.get_short(), opt.get_long()) {
|
||||
(Some(short), Some(long)) => {
|
||||
vec![short_option(short), roman(", "), long_option(long)]
|
||||
}
|
||||
(Some(short), None) => vec![short_option(short)],
|
||||
(None, Some(long)) => vec![long_option(long)],
|
||||
(None, None) => vec![],
|
||||
};
|
||||
|
||||
if opt.get_action().takes_values() {
|
||||
if let Some(value) = &opt.get_value_names() {
|
||||
header.push(roman(" "));
|
||||
header.push(italic(value.join(" ")));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(defs) = option_default_values(opt) {
|
||||
header.push(roman(" "));
|
||||
header.push(roman(defs));
|
||||
}
|
||||
|
||||
let mut body = vec![];
|
||||
let mut arg_help_written = false;
|
||||
if let Some(help) = option_help(opt) {
|
||||
arg_help_written = true;
|
||||
let mut help = help.to_string();
|
||||
if !help.as_str().ends_with('.') {
|
||||
help.push('.');
|
||||
}
|
||||
|
||||
body.push(roman(help));
|
||||
}
|
||||
|
||||
roff.control("TP", []);
|
||||
roff.text(header);
|
||||
roff.text(body);
|
||||
|
||||
if let Some((possible_values_text, with_help)) = get_possible_values(opt) {
|
||||
if arg_help_written {
|
||||
// It looks nice to have a separation between the help and the values
|
||||
roff.text([Inline::LineBreak, Inline::LineBreak]);
|
||||
}
|
||||
if with_help {
|
||||
roff.text([Inline::LineBreak, italic("Possible values:")]);
|
||||
|
||||
// Need to indent twice to get it to look right, because .TP heading indents,
|
||||
// but that indent doesn't Carry over to the .IP for the
|
||||
// bullets. The standard shift size is 7 for terminal devices
|
||||
roff.control("RS", ["14"]);
|
||||
for line in possible_values_text {
|
||||
roff.control("IP", ["\\(bu", "2"]);
|
||||
roff.text([roman(line)]);
|
||||
}
|
||||
roff.control("RE", []);
|
||||
} else {
|
||||
let possible_value_text: Vec<Inline> = vec![
|
||||
Inline::LineBreak,
|
||||
roman("["),
|
||||
italic("possible values: "),
|
||||
roman(possible_values_text.join(", ")),
|
||||
roman("]"),
|
||||
];
|
||||
roff.text(possible_value_text);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(env) = option_environment(opt) {
|
||||
roff.control("RS", []);
|
||||
roff.text(env);
|
||||
roff.control("RE", []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn option_markers(opt: &clap::Arg) -> (&'static str, &'static str) {
|
||||
markers(opt.is_required_set())
|
||||
}
|
||||
|
||||
fn option_markers_single(opt: &clap::Arg) -> (&'static str, &'static str) {
|
||||
if opt.is_required_set() {
|
||||
("", "")
|
||||
} else {
|
||||
markers(opt.is_required_set())
|
||||
}
|
||||
}
|
||||
|
||||
fn markers(required: bool) -> (&'static str, &'static str) {
|
||||
if required {
|
||||
("{", "}")
|
||||
} else {
|
||||
("[", "]")
|
||||
}
|
||||
}
|
||||
|
||||
fn short_option(opt: char) -> Inline {
|
||||
roman(format!("-{opt}"))
|
||||
}
|
||||
|
||||
fn long_option(opt: &str) -> Inline {
|
||||
roman(format!("--{opt}"))
|
||||
}
|
||||
|
||||
fn option_help(opt: &clap::Arg) -> Option<&clap::builder::StyledStr> {
|
||||
if !opt.is_hide_long_help_set() {
|
||||
let long_help = opt.get_long_help();
|
||||
if long_help.is_some() {
|
||||
return long_help;
|
||||
}
|
||||
}
|
||||
if !opt.is_hide_short_help_set() {
|
||||
return opt.get_help();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn option_environment(opt: &clap::Arg) -> Option<Vec<Inline>> {
|
||||
if opt.is_hide_env_set() {
|
||||
return None;
|
||||
} else if let Some(env) = opt.get_env() {
|
||||
return Some(vec![
|
||||
roman("May also be specified with the "),
|
||||
bold(env.to_string_lossy().into_owned()),
|
||||
roman(" environment variable. "),
|
||||
]);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn option_default_values(opt: &clap::Arg) -> Option<String> {
|
||||
if opt.is_hide_default_value_set() || !opt.get_action().takes_values() {
|
||||
return None;
|
||||
} else if !opt.get_default_values().is_empty() {
|
||||
let values = opt
|
||||
.get_default_values()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
return Some(format!("[default: {values}]"));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_possible_values(arg: &clap::Arg) -> Option<(Vec<String>, bool)> {
|
||||
let possibles = &arg.get_possible_values();
|
||||
let possibles: Vec<&clap::builder::PossibleValue> =
|
||||
possibles.iter().filter(|pos| !pos.is_hide_set()).collect();
|
||||
|
||||
if !(possibles.is_empty() || arg.is_hide_possible_values_set()) {
|
||||
return Some(format_possible_values(&possibles));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn format_possible_values(possibles: &Vec<&clap::builder::PossibleValue>) -> (Vec<String>, bool) {
|
||||
let mut lines = vec![];
|
||||
let with_help = possibles.iter().any(|p| p.get_help().is_some());
|
||||
if with_help {
|
||||
for value in possibles {
|
||||
let val_name = value.get_name();
|
||||
match value.get_help() {
|
||||
Some(help) => lines.push(format!(
|
||||
"{val_name}: {help}{period}",
|
||||
period = if help.to_string().ends_with('.') {
|
||||
""
|
||||
} else {
|
||||
"."
|
||||
}
|
||||
)),
|
||||
None => lines.push(val_name.to_string()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines.append(&mut possibles.iter().map(|p| p.get_name().to_string()).collect());
|
||||
}
|
||||
(lines, with_help)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
../rustfmt.toml
|
|
@ -1,571 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub use std::path::PathBuf;
|
||||
|
||||
pub use clap::{builder::TypedValueParser, Args, Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = "mpot",
|
||||
about = "mailing list manager",
|
||||
long_about = "Tool for mailpot mailing list management.",
|
||||
before_long_help = "GNU Affero version 3 or later <https://www.gnu.org/licenses/>",
|
||||
author,
|
||||
version
|
||||
)]
|
||||
pub struct Opt {
|
||||
/// Print logs.
|
||||
#[arg(short, long)]
|
||||
pub debug: bool,
|
||||
/// Configuration file to use.
|
||||
#[arg(short, long, value_parser)]
|
||||
pub config: Option<PathBuf>,
|
||||
#[command(subcommand)]
|
||||
pub cmd: Command,
|
||||
/// Silence all output.
|
||||
#[arg(short, long)]
|
||||
pub quiet: bool,
|
||||
/// Verbose mode (-v, -vv, -vvv, etc).
|
||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||
pub verbose: u8,
|
||||
/// Debug log timestamp (sec, ms, ns, none).
|
||||
#[arg(short, long)]
|
||||
pub ts: Option<stderrlog::Timestamp>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Prints a sample config file to STDOUT.
|
||||
///
|
||||
/// You can generate a new configuration file by writing the output to a
|
||||
/// file, e.g: mpot sample-config --with-smtp > config.toml
|
||||
SampleConfig {
|
||||
/// Use an SMTP connection instead of a shell process.
|
||||
#[arg(long)]
|
||||
with_smtp: bool,
|
||||
},
|
||||
/// Dumps database data to STDOUT.
|
||||
DumpDatabase,
|
||||
/// Lists all registered mailing lists.
|
||||
ListLists,
|
||||
/// Mailing list management.
|
||||
List {
|
||||
/// Selects mailing list to operate on.
|
||||
list_id: String,
|
||||
#[command(subcommand)]
|
||||
cmd: ListCommand,
|
||||
},
|
||||
/// Create new list.
|
||||
CreateList {
|
||||
/// List name.
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
/// List ID.
|
||||
#[arg(long)]
|
||||
id: String,
|
||||
/// List e-mail address.
|
||||
#[arg(long)]
|
||||
address: String,
|
||||
/// List description.
|
||||
#[arg(long)]
|
||||
description: Option<String>,
|
||||
/// List archive URL.
|
||||
#[arg(long)]
|
||||
archive_url: Option<String>,
|
||||
},
|
||||
/// Post message from STDIN to list.
|
||||
Post {
|
||||
/// Show e-mail processing result without actually consuming it.
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
/// Flush outgoing e-mail queue.
|
||||
FlushQueue {
|
||||
/// Show e-mail processing result without actually consuming it.
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
/// Processed mail is stored in queues.
|
||||
Queue {
|
||||
#[arg(long, value_parser = QueueValueParser)]
|
||||
queue: mailpot::queue::Queue,
|
||||
#[command(subcommand)]
|
||||
cmd: QueueCommand,
|
||||
},
|
||||
/// Import a maildir folder into an existing list.
|
||||
ImportMaildir {
|
||||
/// List-ID or primary key value.
|
||||
list_id: String,
|
||||
/// Path to a maildir mailbox.
|
||||
/// Must contain {cur, tmp, new} folders.
|
||||
#[arg(long, value_parser)]
|
||||
maildir_path: PathBuf,
|
||||
},
|
||||
/// Update postfix maps and master.cf (probably needs root permissions).
|
||||
UpdatePostfixConfig {
|
||||
#[arg(short = 'p', long)]
|
||||
/// Override location of master.cf file (default:
|
||||
/// /etc/postfix/master.cf)
|
||||
master_cf: Option<PathBuf>,
|
||||
#[clap(flatten)]
|
||||
config: PostfixConfig,
|
||||
},
|
||||
/// Print postfix maps and master.cf entry to STDOUT.
|
||||
///
|
||||
/// Map output should be added to transport_maps and local_recipient_maps
|
||||
/// parameters in postfix's main.cf. It must be saved in a plain text
|
||||
/// file. To make postfix be able to read them, the postmap application
|
||||
/// must be executed with the path to the map file as its sole argument.
|
||||
///
|
||||
/// postmap /path/to/mylist_maps
|
||||
///
|
||||
/// postmap is usually distributed along with the other postfix binaries.
|
||||
///
|
||||
/// The master.cf entry must be manually appended to the master.cf file. See <https://www.postfix.org/master.5.html>.
|
||||
PrintPostfixConfig {
|
||||
#[clap(flatten)]
|
||||
config: PostfixConfig,
|
||||
},
|
||||
/// All Accounts.
|
||||
Accounts,
|
||||
/// Account info.
|
||||
AccountInfo {
|
||||
/// Account address.
|
||||
address: String,
|
||||
},
|
||||
/// Add account.
|
||||
AddAccount {
|
||||
/// E-mail address.
|
||||
#[arg(long)]
|
||||
address: String,
|
||||
/// SSH public key for authentication.
|
||||
#[arg(long)]
|
||||
password: String,
|
||||
/// Name.
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
/// Public key.
|
||||
#[arg(long)]
|
||||
public_key: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Is account enabled.
|
||||
enabled: Option<bool>,
|
||||
},
|
||||
/// Remove account.
|
||||
RemoveAccount {
|
||||
#[arg(long)]
|
||||
/// E-mail address.
|
||||
address: String,
|
||||
},
|
||||
/// Update account info.
|
||||
UpdateAccount {
|
||||
/// Address to edit.
|
||||
address: String,
|
||||
/// Public key for authentication.
|
||||
#[arg(long)]
|
||||
password: Option<String>,
|
||||
/// Name.
|
||||
#[arg(long)]
|
||||
name: Option<Option<String>>,
|
||||
/// Public key.
|
||||
#[arg(long)]
|
||||
public_key: Option<Option<String>>,
|
||||
#[arg(long)]
|
||||
/// Is account enabled.
|
||||
enabled: Option<Option<bool>>,
|
||||
},
|
||||
/// Show and fix possible data mistakes or inconsistencies.
|
||||
Repair {
|
||||
/// Fix errors (default: false)
|
||||
#[arg(long, default_value = "false")]
|
||||
fix: bool,
|
||||
/// Select all tests (default: false)
|
||||
#[arg(long, default_value = "false")]
|
||||
all: bool,
|
||||
/// Post `datetime` column must have the Date: header value, in RFC2822
|
||||
/// format.
|
||||
#[arg(long, default_value = "false")]
|
||||
datetime_header_value: bool,
|
||||
/// Remove accounts that have no matching subscriptions.
|
||||
#[arg(long, default_value = "false")]
|
||||
remove_empty_accounts: bool,
|
||||
/// Remove subscription requests that have been accepted.
|
||||
#[arg(long, default_value = "false")]
|
||||
remove_accepted_subscription_requests: bool,
|
||||
/// Warn if a list has no owners.
|
||||
#[arg(long, default_value = "false")]
|
||||
warn_list_no_owner: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Postfix config values.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct PostfixConfig {
|
||||
/// User that runs mailpot when postfix relays a message.
|
||||
///
|
||||
/// Must not be the `postfix` user.
|
||||
/// Must have permissions to access the database file and the data
|
||||
/// directory.
|
||||
#[arg(short, long)]
|
||||
pub user: String,
|
||||
/// Group that runs mailpot when postfix relays a message.
|
||||
/// Optional.
|
||||
#[arg(short, long)]
|
||||
pub group: Option<String>,
|
||||
/// The path to the mailpot binary postfix will execute.
|
||||
#[arg(long)]
|
||||
pub binary_path: PathBuf,
|
||||
/// Limit the number of mailpot instances that can exist at the same time.
|
||||
///
|
||||
/// Default is 1.
|
||||
#[arg(long, default_value = "1")]
|
||||
pub process_limit: Option<u64>,
|
||||
/// The directory in which the map files are saved.
|
||||
///
|
||||
/// Default is `data_path` from [`Configuration`](mailpot::Configuration).
|
||||
#[arg(long)]
|
||||
pub map_output_path: Option<PathBuf>,
|
||||
/// The name of the postfix service name to use.
|
||||
/// Default is `mailpot`.
|
||||
///
|
||||
/// A postfix service is a daemon managed by the postfix process.
|
||||
/// Each entry in the `master.cf` configuration file defines a single
|
||||
/// service.
|
||||
///
|
||||
/// The `master.cf` file is documented in [`master(5)`](https://www.postfix.org/master.5.html):
|
||||
/// <https://www.postfix.org/master.5.html>.
|
||||
#[arg(long)]
|
||||
pub transport_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum QueueCommand {
|
||||
/// List.
|
||||
List,
|
||||
/// Print entry in RFC5322 or JSON format.
|
||||
Print {
|
||||
/// index of entry.
|
||||
#[arg(long)]
|
||||
index: Vec<i64>,
|
||||
},
|
||||
/// Delete entry and print it in stdout.
|
||||
Delete {
|
||||
/// index of entry.
|
||||
#[arg(long)]
|
||||
index: Vec<i64>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Subscription options.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct SubscriptionOptions {
|
||||
/// Name.
|
||||
#[arg(long)]
|
||||
pub name: Option<String>,
|
||||
/// Send messages as digest.
|
||||
#[arg(long, default_value = "false")]
|
||||
pub digest: Option<bool>,
|
||||
/// Hide message from list when posting.
|
||||
#[arg(long, default_value = "false")]
|
||||
pub hide_address: Option<bool>,
|
||||
/// Hide message from list when posting.
|
||||
#[arg(long, default_value = "false")]
|
||||
/// E-mail address verification status.
|
||||
pub verified: Option<bool>,
|
||||
#[arg(long, default_value = "true")]
|
||||
/// Receive confirmation email when posting.
|
||||
pub receive_confirmation: Option<bool>,
|
||||
#[arg(long, default_value = "true")]
|
||||
/// Receive posts from list even if address exists in To or Cc header.
|
||||
pub receive_duplicates: Option<bool>,
|
||||
#[arg(long, default_value = "false")]
|
||||
/// Receive own posts from list.
|
||||
pub receive_own_posts: Option<bool>,
|
||||
#[arg(long, default_value = "true")]
|
||||
/// Is subscription enabled.
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Account options.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct AccountOptions {
|
||||
/// Name.
|
||||
#[arg(long)]
|
||||
pub name: Option<String>,
|
||||
/// Public key.
|
||||
#[arg(long)]
|
||||
pub public_key: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Is account enabled.
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ListCommand {
|
||||
/// List subscriptions of list.
|
||||
Subscriptions,
|
||||
/// List subscription requests.
|
||||
SubscriptionRequests,
|
||||
/// Add subscription to list.
|
||||
AddSubscription {
|
||||
/// E-mail address.
|
||||
#[arg(long)]
|
||||
address: String,
|
||||
#[clap(flatten)]
|
||||
subscription_options: SubscriptionOptions,
|
||||
},
|
||||
/// Remove subscription from list.
|
||||
RemoveSubscription {
|
||||
#[arg(long)]
|
||||
/// E-mail address.
|
||||
address: String,
|
||||
},
|
||||
/// Update subscription info.
|
||||
UpdateSubscription {
|
||||
/// Address to edit.
|
||||
address: String,
|
||||
#[clap(flatten)]
|
||||
subscription_options: SubscriptionOptions,
|
||||
},
|
||||
/// Accept a subscription request by its primary key.
|
||||
AcceptSubscriptionRequest {
|
||||
/// The primary key of the request.
|
||||
pk: i64,
|
||||
/// Do not send confirmation e-mail.
|
||||
#[arg(long, default_value = "false")]
|
||||
do_not_send_confirmation: bool,
|
||||
},
|
||||
/// Send subscription confirmation manually.
|
||||
SendConfirmationForSubscription {
|
||||
/// The primary key of the subscription.
|
||||
pk: i64,
|
||||
},
|
||||
/// Add a new post policy.
|
||||
AddPostPolicy {
|
||||
#[arg(long)]
|
||||
/// Only list owners can post.
|
||||
announce_only: bool,
|
||||
#[arg(long)]
|
||||
/// Only subscriptions can post.
|
||||
subscription_only: bool,
|
||||
#[arg(long)]
|
||||
/// Subscriptions can post.
|
||||
/// Other posts must be approved by list owners.
|
||||
approval_needed: bool,
|
||||
#[arg(long)]
|
||||
/// Anyone can post without restrictions.
|
||||
open: bool,
|
||||
#[arg(long)]
|
||||
/// Allow posts, but handle it manually.
|
||||
custom: bool,
|
||||
},
|
||||
// Remove post policy.
|
||||
RemovePostPolicy {
|
||||
#[arg(long)]
|
||||
/// Post policy primary key.
|
||||
pk: i64,
|
||||
},
|
||||
/// Add subscription policy to list.
|
||||
AddSubscriptionPolicy {
|
||||
#[arg(long)]
|
||||
/// Send confirmation e-mail when subscription is finalized.
|
||||
send_confirmation: bool,
|
||||
#[arg(long)]
|
||||
/// Anyone can subscribe without restrictions.
|
||||
open: bool,
|
||||
#[arg(long)]
|
||||
/// Only list owners can manually add subscriptions.
|
||||
manual: bool,
|
||||
#[arg(long)]
|
||||
/// Anyone can request to subscribe.
|
||||
request: bool,
|
||||
#[arg(long)]
|
||||
/// Allow subscriptions, but handle it manually.
|
||||
custom: bool,
|
||||
},
|
||||
RemoveSubscriptionPolicy {
|
||||
#[arg(long)]
|
||||
/// Subscription policy primary key.
|
||||
pk: i64,
|
||||
},
|
||||
/// Add list owner to list.
|
||||
AddListOwner {
|
||||
#[arg(long)]
|
||||
address: String,
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
},
|
||||
RemoveListOwner {
|
||||
#[arg(long)]
|
||||
/// List owner primary key.
|
||||
pk: i64,
|
||||
},
|
||||
/// Alias for update-subscription --enabled true.
|
||||
EnableSubscription {
|
||||
/// Subscription address.
|
||||
address: String,
|
||||
},
|
||||
/// Alias for update-subscription --enabled false.
|
||||
DisableSubscription {
|
||||
/// Subscription address.
|
||||
address: String,
|
||||
},
|
||||
/// Update mailing list details.
|
||||
Update {
|
||||
/// New list name.
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
/// New List-ID.
|
||||
#[arg(long)]
|
||||
id: Option<String>,
|
||||
/// New list address.
|
||||
#[arg(long)]
|
||||
address: Option<String>,
|
||||
/// New list description.
|
||||
#[arg(long)]
|
||||
description: Option<String>,
|
||||
/// New list archive URL.
|
||||
#[arg(long)]
|
||||
archive_url: Option<String>,
|
||||
/// New owner address local part.
|
||||
/// If empty, it defaults to '+owner'.
|
||||
#[arg(long)]
|
||||
owner_local_part: Option<String>,
|
||||
/// New request address local part.
|
||||
/// If empty, it defaults to '+request'.
|
||||
#[arg(long)]
|
||||
request_local_part: Option<String>,
|
||||
/// Require verification of e-mails for new subscriptions.
|
||||
///
|
||||
/// Subscriptions that are initiated from the subscription's address are
|
||||
/// verified automatically.
|
||||
#[arg(long)]
|
||||
verify: Option<bool>,
|
||||
/// Public visibility of list.
|
||||
///
|
||||
/// If hidden, the list will not show up in public APIs unless
|
||||
/// requests to it won't work.
|
||||
#[arg(long)]
|
||||
hidden: Option<bool>,
|
||||
/// Enable or disable the list's functionality.
|
||||
///
|
||||
/// If not enabled, the list will continue to show up in the database
|
||||
/// but e-mails and requests to it won't work.
|
||||
#[arg(long)]
|
||||
enabled: Option<bool>,
|
||||
},
|
||||
/// Show mailing list health status.
|
||||
Health,
|
||||
/// Show mailing list info.
|
||||
Info,
|
||||
/// Import members in a local list from a remote mailman3 REST API instance.
|
||||
///
|
||||
/// To find the id of the remote list, you can check URL/lists.
|
||||
/// Example with curl:
|
||||
///
|
||||
/// curl --anyauth -u admin:pass "http://localhost:9001/3.0/lists"
|
||||
///
|
||||
/// If you're trying to import an entire list, create it first and then
|
||||
/// import its users with this command.
|
||||
///
|
||||
/// Example:
|
||||
/// mpot -c conf.toml list list-general import-members --url "http://localhost:9001/3.0/" --username admin --password password --list-id list-general.example.com --skip-owners --dry-run
|
||||
ImportMembers {
|
||||
#[arg(long)]
|
||||
/// REST HTTP endpoint e.g. http://localhost:9001/3.0/
|
||||
url: String,
|
||||
#[arg(long)]
|
||||
/// REST HTTP Basic Authentication username.
|
||||
username: String,
|
||||
#[arg(long)]
|
||||
/// REST HTTP Basic Authentication password.
|
||||
password: String,
|
||||
#[arg(long)]
|
||||
/// List ID of remote list to query.
|
||||
list_id: String,
|
||||
/// Show what would be inserted without performing any changes.
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
/// Don't import list owners.
|
||||
#[arg(long)]
|
||||
skip_owners: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct QueueValueParser;
|
||||
|
||||
impl QueueValueParser {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedValueParser for QueueValueParser {
|
||||
type Value = mailpot::queue::Queue;
|
||||
|
||||
fn parse_ref(
|
||||
&self,
|
||||
cmd: &clap::Command,
|
||||
arg: Option<&clap::Arg>,
|
||||
value: &std::ffi::OsStr,
|
||||
) -> std::result::Result<Self::Value, clap::Error> {
|
||||
TypedValueParser::parse(self, cmd, arg, value.to_owned())
|
||||
}
|
||||
|
||||
fn parse(
|
||||
&self,
|
||||
cmd: &clap::Command,
|
||||
_arg: Option<&clap::Arg>,
|
||||
value: std::ffi::OsString,
|
||||
) -> std::result::Result<Self::Value, clap::Error> {
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::error::ErrorKind;
|
||||
|
||||
if value.is_empty() {
|
||||
return Err(cmd.clone().error(
|
||||
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand,
|
||||
"queue value required",
|
||||
));
|
||||
}
|
||||
Self::Value::from_str(value.to_str().ok_or_else(|| {
|
||||
cmd.clone().error(
|
||||
ErrorKind::InvalidValue,
|
||||
"Queue value is not an UTF-8 string",
|
||||
)
|
||||
})?)
|
||||
.map_err(|err| cmd.clone().error(ErrorKind::InvalidValue, err))
|
||||
}
|
||||
|
||||
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue>>> {
|
||||
Some(Box::new(
|
||||
mailpot::queue::Queue::possible_values()
|
||||
.iter()
|
||||
.map(clap::builder::PossibleValue::new),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for QueueValueParser {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,149 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2023 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, time::Duration};
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use mailpot::models::{ListOwner, ListSubscription};
|
||||
use ureq::Agent;
|
||||
|
||||
pub struct Mailman3Connection {
|
||||
agent: Agent,
|
||||
url: Cow<'static, str>,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
impl Mailman3Connection {
|
||||
pub fn new(
|
||||
url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let agent: Agent = ureq::AgentBuilder::new()
|
||||
.timeout_read(Duration::from_secs(5))
|
||||
.timeout_write(Duration::from_secs(5))
|
||||
.build();
|
||||
let mut buf = String::new();
|
||||
general_purpose::STANDARD
|
||||
.encode_string(format!("{username}:{password}").as_bytes(), &mut buf);
|
||||
|
||||
let auth: String = format!("Basic {buf}");
|
||||
|
||||
Ok(Self {
|
||||
agent,
|
||||
url: url.trim_end_matches('/').to_string().into(),
|
||||
auth,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn users(&self, list_address: &str) -> Result<Vec<Entry>, Box<dyn std::error::Error>> {
|
||||
let response: String = self
|
||||
.agent
|
||||
.get(&format!(
|
||||
"{}/lists/{list_address}/roster/member?fields=email&fields=display_name",
|
||||
self.url
|
||||
))
|
||||
.set("Authorization", &self.auth)
|
||||
.call()?
|
||||
.into_string()?;
|
||||
Ok(serde_json::from_str::<Roster>(&response)?.entries)
|
||||
}
|
||||
|
||||
pub fn owners(&self, list_address: &str) -> Result<Vec<Entry>, Box<dyn std::error::Error>> {
|
||||
let response: String = self
|
||||
.agent
|
||||
.get(&format!(
|
||||
"{}/lists/{list_address}/roster/owner?fields=email&fields=display_name",
|
||||
self.url
|
||||
))
|
||||
.set("Authorization", &self.auth)
|
||||
.call()?
|
||||
.into_string()?;
|
||||
Ok(serde_json::from_str::<Roster>(&response)?.entries)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct Roster {
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct Entry {
|
||||
display_name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn display_name(&self) -> Option<&str> {
|
||||
if !self.display_name.trim().is_empty() && &self.display_name != "None" {
|
||||
Some(&self.display_name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn email(&self) -> &str {
|
||||
&self.email
|
||||
}
|
||||
|
||||
pub fn into_subscription(self, list: i64) -> ListSubscription {
|
||||
let Self {
|
||||
display_name,
|
||||
email,
|
||||
} = self;
|
||||
|
||||
ListSubscription {
|
||||
pk: -1,
|
||||
list,
|
||||
address: email,
|
||||
name: if !display_name.trim().is_empty() && &display_name != "None" {
|
||||
Some(display_name)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
account: None,
|
||||
enabled: true,
|
||||
verified: true,
|
||||
digest: false,
|
||||
hide_address: false,
|
||||
receive_duplicates: false,
|
||||
receive_own_posts: false,
|
||||
receive_confirmation: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_owner(self, list: i64) -> ListOwner {
|
||||
let Self {
|
||||
display_name,
|
||||
email,
|
||||
} = self;
|
||||
|
||||
ListOwner {
|
||||
pk: -1,
|
||||
list,
|
||||
address: email,
|
||||
name: if !display_name.trim().is_empty() && &display_name != "None" {
|
||||
Some(display_name)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use mailpot::{
|
||||
chrono,
|
||||
melib::{self, Envelope},
|
||||
models::{Account, DbVal, ListSubscription, MailingList},
|
||||
rusqlite, Connection, Result,
|
||||
};
|
||||
|
||||
pub fn datetime_header_value_lint(db: &mut Connection, dry_run: bool) -> Result<()> {
|
||||
let mut col = vec![];
|
||||
{
|
||||
let mut stmt = db.connection.prepare("SELECT * FROM post ORDER BY pk")?;
|
||||
let iter = stmt.query_map([], |row| {
|
||||
let pk: i64 = row.get("pk")?;
|
||||
let date_s: String = row.get("datetime")?;
|
||||
match melib::utils::datetime::rfc822_to_timestamp(date_s.trim()) {
|
||||
Err(_) | Ok(0) => {
|
||||
let mut timestamp: i64 = row.get("timestamp")?;
|
||||
let created: i64 = row.get("created")?;
|
||||
if timestamp == 0 {
|
||||
timestamp = created;
|
||||
}
|
||||
timestamp = std::cmp::min(timestamp, created);
|
||||
let timestamp = if timestamp <= 0 {
|
||||
None
|
||||
} else {
|
||||
// safe because we checked it's not negative or zero above.
|
||||
Some(timestamp as u64)
|
||||
};
|
||||
let message: Vec<u8> = row.get("message")?;
|
||||
Ok(Some((pk, date_s, message, timestamp)))
|
||||
}
|
||||
Ok(_) => Ok(None),
|
||||
}
|
||||
})?;
|
||||
|
||||
for entry in iter {
|
||||
if let Some(s) = entry? {
|
||||
col.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut failures = 0;
|
||||
let tx = if dry_run {
|
||||
None
|
||||
} else {
|
||||
Some(db.connection.transaction()?)
|
||||
};
|
||||
if col.is_empty() {
|
||||
println!("datetime_header_value: ok");
|
||||
} else {
|
||||
println!("datetime_header_value: found {} entries", col.len());
|
||||
println!("pk\tDate value\tshould be");
|
||||
for (pk, val, message, timestamp) in col {
|
||||
let correct = if let Ok(v) =
|
||||
chrono::DateTime::<chrono::FixedOffset>::parse_from_rfc3339(&val)
|
||||
{
|
||||
v.to_rfc2822()
|
||||
} else if let Some(v) = timestamp.map(|t| {
|
||||
melib::utils::datetime::timestamp_to_string(
|
||||
t,
|
||||
Some(melib::utils::datetime::formats::RFC822_DATE),
|
||||
true,
|
||||
)
|
||||
}) {
|
||||
v
|
||||
} else if let Ok(v) =
|
||||
Envelope::from_bytes(&message, None).map(|env| env.date_as_str().to_string())
|
||||
{
|
||||
v
|
||||
} else {
|
||||
failures += 1;
|
||||
println!("{pk}\t{val}\tCould not find any valid date value in the post metadata!");
|
||||
continue;
|
||||
};
|
||||
println!("{pk}\t{val}\t{correct}");
|
||||
if let Some(tx) = tx.as_ref() {
|
||||
tx.execute(
|
||||
"UPDATE post SET datetime = ? WHERE pk = ?",
|
||||
rusqlite::params![&correct, pk],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(tx) = tx {
|
||||
tx.commit()?;
|
||||
}
|
||||
if failures > 0 {
|
||||
println!(
|
||||
"datetime_header_value: {failures} failure{}",
|
||||
if failures == 1 { "" } else { "s" }
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_empty_accounts_lint(db: &mut Connection, dry_run: bool) -> Result<()> {
|
||||
let mut col = vec![];
|
||||
{
|
||||
let mut stmt = db.connection.prepare(
|
||||
"SELECT * FROM account WHERE NOT EXISTS (SELECT 1 FROM subscription AS s WHERE \
|
||||
s.address = address) ORDER BY pk",
|
||||
)?;
|
||||
let iter = stmt.query_map([], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
Account {
|
||||
pk,
|
||||
name: row.get("name")?,
|
||||
address: row.get("address")?,
|
||||
public_key: row.get("public_key")?,
|
||||
password: row.get("password")?,
|
||||
enabled: row.get("enabled")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
|
||||
for entry in iter {
|
||||
let entry = entry?;
|
||||
col.push(entry);
|
||||
}
|
||||
}
|
||||
if col.is_empty() {
|
||||
println!("remove_empty_accounts: ok");
|
||||
} else {
|
||||
let tx = if dry_run {
|
||||
None
|
||||
} else {
|
||||
Some(db.connection.transaction()?)
|
||||
};
|
||||
println!("remove_empty_accounts: found {} entries", col.len());
|
||||
println!("pk\tAddress");
|
||||
for DbVal(Account { pk, address, .. }, _) in &col {
|
||||
println!("{pk}\t{address}");
|
||||
}
|
||||
if let Some(tx) = tx {
|
||||
for DbVal(_, pk) in col {
|
||||
tx.execute("DELETE FROM account WHERE pk = ?", [pk])?;
|
||||
}
|
||||
tx.commit()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_accepted_subscription_requests_lint(
|
||||
db: &mut Connection,
|
||||
dry_run: bool,
|
||||
) -> Result<()> {
|
||||
let mut col = vec![];
|
||||
{
|
||||
let mut stmt = db.connection.prepare(
|
||||
"SELECT * FROM candidate_subscription WHERE accepted IS NOT NULL ORDER BY pk",
|
||||
)?;
|
||||
let iter = stmt.query_map([], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListSubscription {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
account: row.get("account")?,
|
||||
name: row.get("name")?,
|
||||
digest: row.get("digest")?,
|
||||
enabled: row.get("enabled")?,
|
||||
verified: row.get("verified")?,
|
||||
hide_address: row.get("hide_address")?,
|
||||
receive_duplicates: row.get("receive_duplicates")?,
|
||||
receive_own_posts: row.get("receive_own_posts")?,
|
||||
receive_confirmation: row.get("receive_confirmation")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
|
||||
for entry in iter {
|
||||
let entry = entry?;
|
||||
col.push(entry);
|
||||
}
|
||||
}
|
||||
if col.is_empty() {
|
||||
println!("remove_accepted_subscription_requests: ok");
|
||||
} else {
|
||||
let tx = if dry_run {
|
||||
None
|
||||
} else {
|
||||
Some(db.connection.transaction()?)
|
||||
};
|
||||
println!(
|
||||
"remove_accepted_subscription_requests: found {} entries",
|
||||
col.len()
|
||||
);
|
||||
println!("pk\tAddress");
|
||||
for DbVal(ListSubscription { pk, address, .. }, _) in &col {
|
||||
println!("{pk}\t{address}");
|
||||
}
|
||||
if let Some(tx) = tx {
|
||||
for DbVal(_, pk) in col {
|
||||
tx.execute("DELETE FROM candidate_subscription WHERE pk = ?", [pk])?;
|
||||
}
|
||||
tx.commit()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn warn_list_no_owner_lint(db: &mut Connection, _: bool) -> Result<()> {
|
||||
let mut stmt = db.connection.prepare(
|
||||
"SELECT * FROM list WHERE NOT EXISTS (SELECT 1 FROM owner AS o WHERE o.list = pk) ORDER \
|
||||
BY pk",
|
||||
)?;
|
||||
let iter = stmt.query_map([], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
MailingList {
|
||||
pk,
|
||||
name: row.get("name")?,
|
||||
id: row.get("id")?,
|
||||
address: row.get("address")?,
|
||||
description: row.get("description")?,
|
||||
topics: vec![],
|
||||
archive_url: row.get("archive_url")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut col = vec![];
|
||||
for entry in iter {
|
||||
let entry = entry?;
|
||||
col.push(entry);
|
||||
}
|
||||
if col.is_empty() {
|
||||
println!("warn_list_no_owner: ok");
|
||||
} else {
|
||||
println!("warn_list_no_owner: found {} entries", col.len());
|
||||
println!("pk\tName");
|
||||
for DbVal(MailingList { pk, name, .. }, _) in col {
|
||||
println!("{pk}\t{name}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
/*
|
||||
* This file is part of mailpot
|
||||
*
|
||||
* Copyright 2020 - Manos Pitsidianakis
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use mailpot::{melib::smtp, Configuration, Connection, Context, Result};
|
||||
use mailpot_cli::{commands::*, *};
|
||||
|
||||
fn run_app(
|
||||
config: Option<PathBuf>,
|
||||
cmd: Command,
|
||||
debug: bool,
|
||||
quiet: bool,
|
||||
verbose: u8,
|
||||
) -> Result<()> {
|
||||
if let Command::SampleConfig { with_smtp } = cmd {
|
||||
let mut new = Configuration::new("/path/to/sqlite.db");
|
||||
new.administrators.push("admin@example.com".to_string());
|
||||
if with_smtp {
|
||||
new.send_mail = mailpot::SendMail::Smtp(smtp::SmtpServerConf {
|
||||
hostname: "mail.example.com".to_string(),
|
||||
port: 587,
|
||||
envelope_from: "".to_string(),
|
||||
auth: smtp::SmtpAuth::Auto {
|
||||
username: "user".to_string(),
|
||||
password: smtp::Password::Raw("hunter2".to_string()),
|
||||
auth_type: smtp::SmtpAuthType::default(),
|
||||
require_auth: true,
|
||||
},
|
||||
security: smtp::SmtpSecurity::StartTLS {
|
||||
danger_accept_invalid_certs: false,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
});
|
||||
}
|
||||
println!("{}", new.to_toml());
|
||||
return Ok(());
|
||||
};
|
||||
let config_path = if let Some(path) = config.as_deref() {
|
||||
path
|
||||
} else {
|
||||
let mut opt = Opt::command();
|
||||
opt.error(
|
||||
clap::error::ErrorKind::MissingRequiredArgument,
|
||||
"--config is required for mailing list operations",
|
||||
)
|
||||
.exit();
|
||||
};
|
||||
|
||||
let config = Configuration::from_file(config_path).with_context(|| {
|
||||
format!(
|
||||
"Could not read configuration file from path: {}",
|
||||
config_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
use Command::*;
|
||||
let mut db = Connection::open_or_create_db(config)
|
||||
.context("Could not open database connection with this configuration")?
|
||||
.trusted();
|
||||
match cmd {
|
||||
SampleConfig { .. } => {}
|
||||
DumpDatabase => {
|
||||
dump_database(&mut db).context("Could not dump database.")?;
|
||||
}
|
||||
ListLists => {
|
||||
list_lists(&mut db).context("Could not retrieve mailing lists.")?;
|
||||
}
|
||||
List { list_id, cmd } => {
|
||||
list(&mut db, &list_id, cmd, quiet).map_err(|err| {
|
||||
err.chain_err(|| {
|
||||
mailpot::Error::from(format!("Could not perform list command for {list_id}."))
|
||||
})
|
||||
})?;
|
||||
}
|
||||
CreateList {
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
archive_url,
|
||||
} => {
|
||||
create_list(&mut db, name, id, address, description, archive_url, quiet)
|
||||
.context("Could not create list.")?;
|
||||
}
|
||||
Post { dry_run } => {
|
||||
post(&mut db, dry_run, debug).context("Could not process post.")?;
|
||||
}
|
||||
FlushQueue { dry_run } => {
|
||||
flush_queue(&mut db, dry_run, verbose, debug).with_context(|| {
|
||||
format!("Could not flush queue {}.", mailpot::queue::Queue::Out)
|
||||
})?;
|
||||
}
|
||||
Queue { queue, cmd } => {
|
||||
queue_(&mut db, queue, cmd, quiet)
|
||||
.with_context(|| format!("Could not perform queue command for queue `{queue}`."))?;
|
||||
}
|
||||
ImportMaildir {
|
||||
list_id,
|
||||
maildir_path,
|
||||
} => {
|
||||
import_maildir(
|
||||
&mut db,
|
||||
&list_id,
|
||||
maildir_path.clone(),
|
||||
quiet,
|
||||
debug,
|
||||
verbose,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not import maildir path {} to list `{list_id}`.",
|
||||
maildir_path.display(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
UpdatePostfixConfig { master_cf, config } => {
|
||||
update_postfix_config(config_path, &mut db, master_cf, config)
|
||||
.context("Could not update postfix configuration.")?;
|
||||
}
|
||||
PrintPostfixConfig { config } => {
|
||||
print_postfix_config(config_path, &mut db, config)
|
||||
.context("Could not print postfix configuration.")?;
|
||||
}
|
||||
Accounts => {
|
||||
accounts(&mut db, quiet).context("Could not retrieve accounts.")?;
|
||||
}
|
||||
AccountInfo { address } => {
|
||||
account_info(&mut db, &address, quiet).with_context(|| {
|
||||
format!("Could not retrieve account info for address {address}.")
|
||||
})?;
|
||||
}
|
||||
AddAccount {
|
||||
address,
|
||||
password,
|
||||
name,
|
||||
public_key,
|
||||
enabled,
|
||||
} => {
|
||||
add_account(&mut db, address, password, name, public_key, enabled)
|
||||
.context("Could not add account.")?;
|
||||
}
|
||||
RemoveAccount { address } => {
|
||||
remove_account(&mut db, &address, quiet)
|
||||
.with_context(|| format!("Could not remove account with address {address}."))?;
|
||||
}
|
||||
UpdateAccount {
|
||||
address,
|
||||
password,
|
||||
name,
|
||||
public_key,
|
||||
enabled,
|
||||
} => {
|
||||
update_account(&mut db, address, password, name, public_key, enabled)
|
||||
.context("Could not update account.")?;
|
||||
}
|
||||
Repair {
|
||||
fix,
|
||||
all,
|
||||
datetime_header_value,
|
||||
remove_empty_accounts,
|
||||
remove_accepted_subscription_requests,
|
||||
warn_list_no_owner,
|
||||
} => {
|
||||
repair(
|
||||
&mut db,
|
||||
fix,
|
||||
all,
|
||||
datetime_header_value,
|
||||
remove_empty_accounts,
|
||||
remove_accepted_subscription_requests,
|
||||
warn_list_no_owner,
|
||||
)
|
||||
.context("Could not perform database repair.")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> std::result::Result<(), i32> {
|
||||
let opt = Opt::parse();
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.module("mailpot")
|
||||
.quiet(opt.quiet)
|
||||
.verbosity(opt.verbose as usize)
|
||||
.timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off))
|
||||
.init()
|
||||
.unwrap();
|
||||
if opt.debug {
|
||||
println!("DEBUG: {:?}", &opt);
|
||||
}
|
||||
let Opt {
|
||||
config,
|
||||
cmd,
|
||||
debug,
|
||||
quiet,
|
||||
verbose,
|
||||
..
|
||||
} = opt;
|
||||
if let Err(err) = run_app(config, cmd, debug, quiet, verbose) {
|
||||
print!("{}", err.display_chain());
|
||||
std::process::exit(-1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,268 +0,0 @@
|
|||
/*
|
||||
* meli - email module
|
||||
*
|
||||
* Copyright 2019 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![deny(dead_code)]
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use assert_cmd::{assert::OutputAssertExt, Command};
|
||||
use mailpot::{models::*, Configuration, Connection, SendMail};
|
||||
use predicates::prelude::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_cli_basic_interfaces() {
|
||||
fn no_args() {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
// 2 -> incorrect usage
|
||||
cmd.assert().code(2);
|
||||
}
|
||||
|
||||
fn version() {
|
||||
// --version is successful
|
||||
for arg in ["--version", "-V"] {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd.arg(arg).output().unwrap().assert();
|
||||
output.code(0).stdout(predicates::str::starts_with("mpot "));
|
||||
}
|
||||
}
|
||||
|
||||
fn help() {
|
||||
// --help is successful
|
||||
for (arg, starts_with) in [
|
||||
("--help", "GNU Affero version 3 or later"),
|
||||
("-h", "mailing list manager"),
|
||||
] {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd.arg(arg).output().unwrap().assert();
|
||||
output
|
||||
.code(0)
|
||||
.stdout(predicates::str::starts_with(starts_with))
|
||||
.stdout(predicates::str::contains("Usage:"));
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_config() {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
// sample-config does not require a configuration file as an argument (but other
|
||||
// commands do)
|
||||
let output = cmd.arg("sample-config").output().unwrap().assert();
|
||||
output.code(0).stdout(predicates::str::is_empty().not());
|
||||
}
|
||||
|
||||
fn config_required() {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd.arg("list-lists").output().unwrap().assert();
|
||||
output.code(2).stdout(predicates::str::is_empty()).stderr(
|
||||
predicate::eq(
|
||||
r#"error: --config is required for mailing list operations
|
||||
|
||||
Usage: mpot [OPTIONS] <COMMAND>
|
||||
|
||||
For more information, try '--help'."#,
|
||||
)
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
}
|
||||
|
||||
no_args();
|
||||
version();
|
||||
help();
|
||||
sample_config();
|
||||
config_required();
|
||||
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let conf_path = tmp_dir.path().join("conf.toml");
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
db_path,
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
administrators: vec![],
|
||||
};
|
||||
|
||||
let config_str = config.to_toml();
|
||||
|
||||
fn config_not_exists(conf: &Path) {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-c")
|
||||
.arg(conf)
|
||||
.arg("list-lists")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(255).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq(
|
||||
format!(
|
||||
"[1] Could not read configuration file from path: {path} Caused by:\n[2] \
|
||||
Configuration file {path} not found. Caused by:\n[3] Error returned from \
|
||||
internal I/O operation: No such file or directory (os error 2)",
|
||||
path = conf.display()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
}
|
||||
|
||||
config_not_exists(&conf_path);
|
||||
|
||||
std::fs::write(&conf_path, config_str.as_bytes()).unwrap();
|
||||
|
||||
fn list_lists(conf: &Path, eq: &str) {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-c")
|
||||
.arg(conf)
|
||||
.arg("list-lists")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output
|
||||
.code(0)
|
||||
.stderr(predicates::str::is_empty())
|
||||
.stdout(predicate::eq(eq).trim().normalize());
|
||||
}
|
||||
|
||||
list_lists(&conf_path, "No lists found.");
|
||||
|
||||
{
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
|
||||
let foo_chat = db
|
||||
.create_list(MailingList {
|
||||
pk: 0,
|
||||
name: "foobar chat".into(),
|
||||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
topics: vec![],
|
||||
description: None,
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(foo_chat.pk(), 1);
|
||||
}
|
||||
list_lists(
|
||||
&conf_path,
|
||||
"- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
|
||||
\"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
|
||||
owners: None\n\tPost policy: None\n\tSubscription policy: None",
|
||||
);
|
||||
|
||||
fn create_list(conf: &Path) {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-c")
|
||||
.arg(conf)
|
||||
.arg("create-list")
|
||||
.arg("--name")
|
||||
.arg("twobar")
|
||||
.arg("--id")
|
||||
.arg("twobar-chat")
|
||||
.arg("--address")
|
||||
.arg("twobar-chat@example.com")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq("Created new list \"twobar-chat\" with primary key 2")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
}
|
||||
create_list(&conf_path);
|
||||
list_lists(
|
||||
&conf_path,
|
||||
"- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
|
||||
\"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
|
||||
owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
|
||||
DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
|
||||
\"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
|
||||
2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None",
|
||||
);
|
||||
|
||||
fn add_list_owner(conf: &Path) {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-c")
|
||||
.arg(conf)
|
||||
.arg("list")
|
||||
.arg("twobar-chat")
|
||||
.arg("add-list-owner")
|
||||
.arg("--address")
|
||||
.arg("list-owner@example.com")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq("Added new list owner [#1 2] list-owner@example.com")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
}
|
||||
add_list_owner(&conf_path);
|
||||
list_lists(
|
||||
&conf_path,
|
||||
"- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
|
||||
\"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
|
||||
owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
|
||||
DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
|
||||
\"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
|
||||
2)\n\tList owners:\n\t- [#1 2] list-owner@example.com\n\tPost policy: \
|
||||
None\n\tSubscription policy: None",
|
||||
);
|
||||
|
||||
fn remove_list_owner(conf: &Path) {
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-c")
|
||||
.arg(conf)
|
||||
.arg("list")
|
||||
.arg("twobar-chat")
|
||||
.arg("remove-list-owner")
|
||||
.arg("--pk")
|
||||
.arg("1")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq("Removed list owner with pk = 1")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
}
|
||||
remove_list_owner(&conf_path);
|
||||
list_lists(
|
||||
&conf_path,
|
||||
"- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
|
||||
\"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
|
||||
owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
|
||||
DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
|
||||
\"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
|
||||
2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None",
|
||||
);
|
||||
}
|
|
@ -1,398 +0,0 @@
|
|||
/*
|
||||
* meli - email module
|
||||
*
|
||||
* Copyright 2019 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use mailpot::{
|
||||
melib,
|
||||
models::{changesets::ListSubscriptionChangeset, *},
|
||||
queue::Queue,
|
||||
Configuration, Connection, SendMail,
|
||||
};
|
||||
use mailpot_tests::*;
|
||||
use predicates::prelude::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn generate_mail(from: &str, to: &str, subject: &str, body: &str, seq: &mut usize) -> String {
|
||||
format!(
|
||||
"From: {from}@example.com
|
||||
To: <foo-chat{to}@example.com>
|
||||
Subject: {subject}
|
||||
Date: Thu, 29 Oct 2020 13:58:16 +0000
|
||||
Message-ID:
|
||||
<aaa{}@example.com>
|
||||
Content-Language: en-US
|
||||
Content-Type: text/plain
|
||||
|
||||
{body}
|
||||
",
|
||||
{
|
||||
let val = *seq;
|
||||
*seq += 1;
|
||||
val
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_queue_flush() {
|
||||
use assert_cmd::Command;
|
||||
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let conf_path = tmp_dir.path().join("conf.toml");
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let smtp_handler = TestSmtpHandler::builder().address("127.0.0.1:8826").build();
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::Smtp(smtp_handler.smtp_conf()),
|
||||
db_path,
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
administrators: vec![],
|
||||
};
|
||||
|
||||
let config_str = config.to_toml();
|
||||
|
||||
std::fs::write(&conf_path, config_str.as_bytes()).unwrap();
|
||||
|
||||
log::info!("Creating foo-chat@example.com mailing list.");
|
||||
let post_policy;
|
||||
let foo_chat = {
|
||||
let db = Connection::open_or_create_db(config.clone())
|
||||
.unwrap()
|
||||
.trusted();
|
||||
|
||||
let foo_chat = db
|
||||
.create_list(MailingList {
|
||||
pk: 0,
|
||||
name: "foobar chat".into(),
|
||||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(foo_chat.pk(), 1);
|
||||
post_policy = db
|
||||
.set_list_post_policy(PostPolicy {
|
||||
pk: -1,
|
||||
list: foo_chat.pk(),
|
||||
announce_only: false,
|
||||
subscription_only: false,
|
||||
approval_needed: false,
|
||||
open: true,
|
||||
custom: false,
|
||||
})
|
||||
.unwrap();
|
||||
foo_chat
|
||||
};
|
||||
|
||||
let headers_fn = |env: &melib::Envelope| {
|
||||
assert!(env.subject().starts_with(&format!("[{}] ", foo_chat.id)));
|
||||
let headers = env.other_headers();
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_ID)
|
||||
.map(|header| header.to_string()),
|
||||
Some(foo_chat.id_header())
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_HELP)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.help_header()
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_POST)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.post_header(Some(&post_policy))
|
||||
);
|
||||
};
|
||||
|
||||
log::info!("Running mpot flush-queue on empty out queue.");
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-vv")
|
||||
.arg("-c")
|
||||
.arg(&conf_path)
|
||||
.arg("flush-queue")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq("Queue out has 0 messages.")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
|
||||
let mut seq = 0; // for generated emails
|
||||
log::info!("Subscribe two users, Αλίκη and Χαραλάμπης to foo-chat.");
|
||||
|
||||
{
|
||||
let db = Connection::open_or_create_db(config.clone())
|
||||
.unwrap()
|
||||
.trusted();
|
||||
|
||||
for who in ["Αλίκη", "Χαραλάμπης"] {
|
||||
// = ["Alice", "Bob"]
|
||||
let mail = generate_mail(who, "+request", "subscribe", "", &mut seq);
|
||||
let subenvelope = mailpot::melib::Envelope::from_bytes(mail.as_bytes(), None)
|
||||
.expect("Could not parse message");
|
||||
db.post(&subenvelope, mail.as_bytes(), /* dry_run */ false)
|
||||
.unwrap();
|
||||
}
|
||||
db.update_subscription(ListSubscriptionChangeset {
|
||||
list: foo_chat.pk(),
|
||||
address: "Χαραλάμπης@example.com".into(),
|
||||
receive_own_posts: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
let out_queue = db.queue(Queue::Out).unwrap();
|
||||
assert_eq!(out_queue.len(), 2);
|
||||
assert_eq!(db.list_subscriptions(foo_chat.pk()).unwrap().len(), 2);
|
||||
assert_eq!(db.queue(Queue::Error).unwrap().len(), 0);
|
||||
}
|
||||
|
||||
log::info!("Flush out queue, subscription confirmations should be sent to the new users.");
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-vv")
|
||||
.arg("-c")
|
||||
.arg(&conf_path)
|
||||
.arg("flush-queue")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stdout(
|
||||
predicate::eq("Queue out has 2 messages.")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
|
||||
/* Check that confirmation emails are correct */
|
||||
let stored = std::mem::take(&mut *smtp_handler.stored.lock().unwrap());
|
||||
assert_eq!(stored.len(), 2);
|
||||
assert_eq!(stored[0].0, "=?UTF-8?B?zpHOu86vzrrOtw==?=@example.com");
|
||||
assert_eq!(
|
||||
stored[1].0,
|
||||
"=?UTF-8?B?zqfOsc+BzrHOu86szrzPgM63z4I=?=@example.com"
|
||||
);
|
||||
for item in stored.iter() {
|
||||
assert_eq!(
|
||||
item.1.subject(),
|
||||
"[foo-chat] You have successfully subscribed to foobar chat."
|
||||
);
|
||||
assert_eq!(
|
||||
&item.1.field_from_to_string(),
|
||||
"foo-chat+request@example.com"
|
||||
);
|
||||
headers_fn(&item.1);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Χαραλάμπης submits a post to list. Flush out queue, Χαραλάμπης' post should be relayed \
|
||||
to Αλίκη, and Χαραλάμπης should receive a copy of their own post because of \
|
||||
`receive_own_posts` setting."
|
||||
);
|
||||
|
||||
{
|
||||
let db = Connection::open_or_create_db(config.clone())
|
||||
.unwrap()
|
||||
.trusted();
|
||||
let mail = generate_mail("Χαραλάμπης", "", "hello world", "Hello there.", &mut seq);
|
||||
let subenvelope = mailpot::melib::Envelope::from_bytes(mail.as_bytes(), None)
|
||||
.expect("Could not parse message");
|
||||
db.post(&subenvelope, mail.as_bytes(), /* dry_run */ false)
|
||||
.unwrap();
|
||||
let out_queue = db.queue(Queue::Out).unwrap();
|
||||
assert_eq!(out_queue.len(), 2);
|
||||
}
|
||||
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-vv")
|
||||
.arg("-c")
|
||||
.arg(&conf_path)
|
||||
.arg("flush-queue")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stdout(
|
||||
predicate::eq("Queue out has 2 messages.")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
|
||||
/* Check that user posts are correct */
|
||||
{
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
|
||||
let out_queue = db.queue(Queue::Out).unwrap();
|
||||
assert_eq!(out_queue.len(), 0);
|
||||
}
|
||||
|
||||
let stored = std::mem::take(&mut *smtp_handler.stored.lock().unwrap());
|
||||
assert_eq!(stored.len(), 2);
|
||||
assert_eq!(stored[0].0, "Αλίκη@example.com");
|
||||
assert_eq!(stored[1].0, "Χαραλάμπης@example.com");
|
||||
assert_eq!(stored[0].1.message_id(), stored[1].1.message_id());
|
||||
assert_eq!(stored[0].1.other_headers(), stored[1].1.other_headers());
|
||||
headers_fn(&stored[0].1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_requests_submission() {
|
||||
use assert_cmd::Command;
|
||||
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
|
||||
let conf_path = tmp_dir.path().join("conf.toml");
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let smtp_handler = TestSmtpHandler::builder().address("127.0.0.1:8827").build();
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::Smtp(smtp_handler.smtp_conf()),
|
||||
db_path,
|
||||
data_path: tmp_dir.path().to_path_buf(),
|
||||
administrators: vec![],
|
||||
};
|
||||
|
||||
let config_str = config.to_toml();
|
||||
|
||||
std::fs::write(&conf_path, config_str.as_bytes()).unwrap();
|
||||
|
||||
log::info!("Creating foo-chat@example.com mailing list.");
|
||||
let post_policy;
|
||||
let foo_chat = {
|
||||
let db = Connection::open_or_create_db(config.clone())
|
||||
.unwrap()
|
||||
.trusted();
|
||||
|
||||
let foo_chat = db
|
||||
.create_list(MailingList {
|
||||
pk: 0,
|
||||
name: "foobar chat".into(),
|
||||
id: "foo-chat".into(),
|
||||
address: "foo-chat@example.com".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
archive_url: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(foo_chat.pk(), 1);
|
||||
post_policy = db
|
||||
.set_list_post_policy(PostPolicy {
|
||||
pk: -1,
|
||||
list: foo_chat.pk(),
|
||||
announce_only: false,
|
||||
subscription_only: false,
|
||||
approval_needed: false,
|
||||
open: true,
|
||||
custom: false,
|
||||
})
|
||||
.unwrap();
|
||||
foo_chat
|
||||
};
|
||||
|
||||
let headers_fn = |env: &melib::Envelope| {
|
||||
let headers = env.other_headers();
|
||||
|
||||
assert_eq!(
|
||||
headers.get(melib::HeaderName::LIST_ID),
|
||||
Some(foo_chat.id_header().as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_HELP)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.help_header()
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_POST)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.post_header(Some(&post_policy))
|
||||
);
|
||||
};
|
||||
|
||||
log::info!("Running mpot flush-queue on empty out queue.");
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-vv")
|
||||
.arg("-c")
|
||||
.arg(&conf_path)
|
||||
.arg("flush-queue")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stderr(predicates::str::is_empty()).stdout(
|
||||
predicate::eq("Queue out has 0 messages.")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
|
||||
let mut seq = 0; // for generated emails
|
||||
log::info!("User Αλίκη sends to foo-chat+request with subject 'help'.");
|
||||
|
||||
{
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
|
||||
let mail = generate_mail("Αλίκη", "+request", "help", "", &mut seq);
|
||||
let subenvelope = mailpot::melib::Envelope::from_bytes(mail.as_bytes(), None)
|
||||
.expect("Could not parse message");
|
||||
db.post(&subenvelope, mail.as_bytes(), /* dry_run */ false)
|
||||
.unwrap();
|
||||
let out_queue = db.queue(Queue::Out).unwrap();
|
||||
assert_eq!(out_queue.len(), 1);
|
||||
assert_eq!(db.list_subscriptions(foo_chat.pk()).unwrap().len(), 0);
|
||||
assert_eq!(db.queue(Queue::Error).unwrap().len(), 0);
|
||||
}
|
||||
|
||||
log::info!("Flush out queue, help reply should go to Αλίκη.");
|
||||
let mut cmd = Command::cargo_bin("mpot").unwrap();
|
||||
let output = cmd
|
||||
.arg("-vv")
|
||||
.arg("-c")
|
||||
.arg(&conf_path)
|
||||
.arg("flush-queue")
|
||||
.output()
|
||||
.unwrap()
|
||||
.assert();
|
||||
output.code(0).stdout(
|
||||
predicate::eq("Queue out has 1 messages.")
|
||||
.trim()
|
||||
.normalize(),
|
||||
);
|
||||
|
||||
/* Check that help email is correct */
|
||||
let stored = std::mem::take(&mut *smtp_handler.stored.lock().unwrap());
|
||||
assert_eq!(stored.len(), 1);
|
||||
assert_eq!(stored[0].0, "=?UTF-8?B?zpHOu86vzrrOtw==?=@example.com");
|
||||
assert_eq!(stored[0].1.subject(), "Help for foobar chat");
|
||||
assert_eq!(
|
||||
&stored[0].1.field_from_to_string(),
|
||||
"foo-chat+request@example.com"
|
||||
);
|
||||
headers_fn(&stored[0].1);
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
.env
|
||||
config/local.json
|
|
@ -1,49 +0,0 @@
|
|||
[package]
|
||||
name = "mailpot-http"
|
||||
version = "0.1.1"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2021"
|
||||
license = "LICENSE"
|
||||
readme = "README.md"
|
||||
description = "mailing list manager"
|
||||
repository = "https://github.com/meli/mailpot"
|
||||
keywords = ["mail", "mailing-lists"]
|
||||
categories = ["email"]
|
||||
default-run = "mpot-http"
|
||||
|
||||
[[bin]]
|
||||
name = "mpot-http"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
axum = { version = "0.6", features = ["headers"] }
|
||||
axum-extra = { version = "^0.7", features = ["typed-routing"] }
|
||||
#jsonwebtoken = "8.3"
|
||||
bcrypt = "0.14"
|
||||
config = "0.13"
|
||||
http = "0.2"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
mailpot-web = { version = "^0.1", path = "../mailpot-web" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
stderrlog = { version = "^0.6" }
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower-http = { version = "0.4", features = [
|
||||
"trace",
|
||||
"compression-br",
|
||||
"propagate-header",
|
||||
"sensitive-headers",
|
||||
"cors",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert-json-diff = "2"
|
||||
hyper = { version = "0.14" }
|
||||
mailpot-tests = { version = "^0.1", path = "../mailpot-tests" }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tempfile = { version = "3.9" }
|
||||
tower = { version = "^0.4" }
|
|
@ -1,2 +0,0 @@
|
|||
# mailpot REST http server
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"environment": "development",
|
||||
"server": {
|
||||
"port": 8080
|
||||
},
|
||||
"auth": {
|
||||
"secret": "secret"
|
||||
},
|
||||
"logger": {
|
||||
"level": "debug"
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"environment": "production",
|
||||
"logger": {
|
||||
"level": "info"
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"environment": "test",
|
||||
"server": {
|
||||
"port": 8088
|
||||
},
|
||||
"logger": {
|
||||
"level": "error"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
../rustfmt.toml
|
|
@ -1,98 +0,0 @@
|
|||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use bcrypt::BcryptError;
|
||||
use serde_json::json;
|
||||
use tokio::task::JoinError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("...")]
|
||||
pub enum Error {
|
||||
#[error("Error parsing ObjectID {0}")]
|
||||
ParseObjectID(String),
|
||||
|
||||
#[error("{0}")]
|
||||
Authenticate(#[from] AuthenticateError),
|
||||
|
||||
#[error("{0}")]
|
||||
BadRequest(#[from] BadRequest),
|
||||
|
||||
#[error("{0}")]
|
||||
NotFound(#[from] NotFound),
|
||||
|
||||
#[error("{0}")]
|
||||
RunSyncTask(#[from] JoinError),
|
||||
|
||||
#[error("{0}")]
|
||||
HashPassword(#[from] BcryptError),
|
||||
|
||||
#[error("{0}")]
|
||||
System(#[from] mailpot::Error),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn get_codes(&self) -> (StatusCode, u16) {
|
||||
match *self {
|
||||
// 4XX Errors
|
||||
Error::ParseObjectID(_) => (StatusCode::BAD_REQUEST, 40001),
|
||||
Error::BadRequest(_) => (StatusCode::BAD_REQUEST, 40002),
|
||||
Error::NotFound(_) => (StatusCode::NOT_FOUND, 40003),
|
||||
Error::Authenticate(AuthenticateError::WrongCredentials) => {
|
||||
(StatusCode::UNAUTHORIZED, 40004)
|
||||
}
|
||||
Error::Authenticate(AuthenticateError::InvalidToken) => {
|
||||
(StatusCode::UNAUTHORIZED, 40005)
|
||||
}
|
||||
Error::Authenticate(AuthenticateError::Locked) => (StatusCode::LOCKED, 40006),
|
||||
|
||||
// 5XX Errors
|
||||
Error::Authenticate(AuthenticateError::TokenCreation) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, 5001)
|
||||
}
|
||||
Error::RunSyncTask(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5005),
|
||||
Error::HashPassword(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5006),
|
||||
Error::System(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5007),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bad_request() -> Self {
|
||||
Error::BadRequest(BadRequest {})
|
||||
}
|
||||
|
||||
pub fn not_found() -> Self {
|
||||
Error::NotFound(NotFound {})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
let (status_code, code) = self.get_codes();
|
||||
let message = self.to_string();
|
||||
let body = Json(json!({ "code": code, "message": message }));
|
||||
|
||||
(status_code, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("...")]
|
||||
pub enum AuthenticateError {
|
||||
#[error("Wrong authentication credentials")]
|
||||
WrongCredentials,
|
||||
#[error("Failed to create authentication token")]
|
||||
TokenCreation,
|
||||
#[error("Invalid authentication credentials")]
|
||||
InvalidToken,
|
||||
#[error("User is locked")]
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Bad Request")]
|
||||
pub struct BadRequest {}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Not found")]
|
||||
pub struct NotFound {}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue