core: reorganise old module hierarchy

axum-login-upgrade
Manos Pitsidianakis 2023-05-03 11:15:55 +03:00
parent fedb766942
commit 9eaa580af4
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
20 changed files with 316 additions and 256 deletions

View File

@ -55,8 +55,12 @@ 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 > config.toml
SampleConfig,
/// 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.

View File

@ -27,9 +27,10 @@ use std::{
mod lints;
use lints::*;
use mailpot::{
melib::{backends::maildir::MaildirPathTrait, smol, Envelope, EnvelopeHash},
melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash},
models::{changesets::*, *},
*,
queue::{Queue, QueueEntry},
Configuration, Connection, Error, ErrorKind, Result, *,
};
use mailpot_cli::*;
@ -60,8 +61,27 @@ 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());
if let Command::SampleConfig { with_smtp } = opt.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(SmtpServerConf {
hostname: "mail.example.com".to_string(),
port: 587,
envelope_from: "".to_string(),
auth: SmtpAuth::Auto {
username: "user".to_string(),
password: Password::Raw("hunter2".to_string()),
auth_type: SmtpAuthType::default(),
require_auth: true,
},
security: SmtpSecurity::StartTLS {
danger_accept_invalid_certs: false,
},
extensions: Default::default(),
});
}
println!("{}", new.to_toml());
return Ok(());
};
let config_path = if let Some(path) = opt.config.as_ref() {
@ -80,7 +100,7 @@ fn run_app(opt: Opt) -> Result<()> {
use Command::*;
let mut db = Connection::open_or_create_db(config)?.trusted();
match opt.cmd {
SampleConfig => {}
SampleConfig { .. } => {}
DumpDatabase => {
let lists = db.lists()?;
let mut stdout = std::io::stdout();

View File

@ -23,7 +23,8 @@ use assert_cmd::assert::OutputAssertExt;
use mailpot::{
melib,
models::{changesets::ListSubscriptionChangeset, *},
Configuration, Connection, Queue, SendMail,
queue::Queue,
Configuration, Connection, SendMail,
};
use mailpot_tests::*;
use predicates::prelude::*;

View File

@ -24,18 +24,20 @@ use std::{
process::{Command, Stdio},
};
use melib::Envelope;
use models::changesets::*;
use log::{info, trace};
use rusqlite::{Connection as DbConnection, OptionalExtension};
use super::{Configuration, *};
use crate::ErrorKind::*;
use crate::{
config::Configuration,
errors::{ErrorKind::*, *},
models::{changesets::MailingListChangeset, DbVal, ListOwner, MailingList, Post},
};
/// A connection to a `mailpot` database.
pub struct Connection {
/// The `rusqlite` connection handle.
pub connection: DbConnection,
conf: Configuration,
pub(crate) conf: Configuration,
}
impl std::fmt::Debug for Connection {
@ -61,17 +63,6 @@ impl Drop for Connection {
}
}
mod templates;
pub use templates::*;
mod queue;
pub use queue::*;
mod posts;
pub use posts::*;
mod subscriptions;
pub use subscriptions::*;
mod policies;
pub use policies::*;
fn log_callback(error_code: std::ffi::c_int, message: &str) {
match error_code {
rusqlite::ffi::SQLITE_OK
@ -569,18 +560,4 @@ impl Connection {
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),
]
}
}

View File

@ -1,178 +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/>.
*/
//! Named templates, for generated e-mail like confirmations, alerts etc.
use super::*;
impl Connection {
/// Fetch all.
pub fn fetch_templates(&self) -> Result<Vec<DbVal<Template>>> {
let mut stmt = self
.connection
.prepare("SELECT * FROM templates ORDER BY pk;")?;
let iter = stmt.query_map(rusqlite::params![], |row| {
let pk = row.get("pk")?;
Ok(DbVal(
Template {
pk,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
},
pk,
))
})?;
let mut ret = vec![];
for templ in iter {
let templ = templ?;
ret.push(templ);
}
Ok(ret)
}
/// Fetch a named template.
pub fn fetch_template(
&self,
template: &str,
list_pk: Option<i64>,
) -> Result<Option<DbVal<Template>>> {
let mut stmt = self
.connection
.prepare("SELECT * FROM templates WHERE name = ? AND list IS ?;")?;
let ret = stmt
.query_row(rusqlite::params![&template, &list_pk], |row| {
let pk = row.get("pk")?;
Ok(DbVal(
Template {
pk,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
},
pk,
))
})
.optional()?;
if ret.is_none() && list_pk.is_some() {
let mut stmt = self
.connection
.prepare("SELECT * FROM templates WHERE name = ? AND list IS NULL;")?;
Ok(stmt
.query_row(rusqlite::params![&template], |row| {
let pk = row.get("pk")?;
Ok(DbVal(
Template {
pk,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
},
pk,
))
})
.optional()?)
} else {
Ok(ret)
}
}
/// Insert a named template.
pub fn add_template(&self, template: Template) -> Result<DbVal<Template>> {
let mut stmt = self.connection.prepare(
"INSERT INTO templates(name, list, subject, headers_json, body) VALUES(?, ?, ?, ?, ?) \
RETURNING *;",
)?;
let ret = stmt
.query_row(
rusqlite::params![
&template.name,
&template.list,
&template.subject,
&template.headers_json,
&template.body
],
|row| {
let pk = row.get("pk")?;
Ok(DbVal(
Template {
pk,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
},
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_template {:?}.", &ret);
Ok(ret)
}
/// Remove a named template.
pub fn remove_template(&self, template: &str, list_pk: Option<i64>) -> Result<Template> {
let mut stmt = self
.connection
.prepare("DELETE FROM templates WHERE name = ? AND list IS ? RETURNING *;")?;
let ret = stmt.query_row(rusqlite::params![&template, &list_pk], |row| {
Ok(Template {
pk: -1,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
})
})?;
trace!(
"remove_template {} list_pk {:?} {:?}.",
template,
&list_pk,
&ret
);
Ok(ret)
}
}

View File

@ -165,20 +165,23 @@ pub extern crate log;
pub extern crate melib;
pub extern crate serde_json;
use log::{info, trace};
mod config;
mod db;
mod connection;
mod errors;
pub mod mail;
pub mod message_filters;
pub mod models;
pub mod policies;
#[cfg(not(target_os = "windows"))]
pub mod postfix;
pub mod posts;
pub mod queue;
pub mod submission;
pub mod subscriptions;
mod templates;
pub use config::{Configuration, SendMail};
pub use db::*;
pub use connection::*;
pub use errors::*;
use models::*;
pub use templates::*;

View File

@ -20,11 +20,13 @@
//! Types for processing new posts: [`PostFilter`](message_filters::PostFilter),
//! [`ListContext`], [`MailJob`] and [`PostAction`].
use log::trace;
use melib::Address;
use super::*;
pub mod message_filters;
use crate::{
models::{ListOwner, ListSubscription, MailingList, PostPolicy, SubscriptionPolicy},
DbVal,
};
/// Post action returned from a list's
/// [`PostFilter`](message_filters::PostFilter) stack.
#[derive(Debug)]
@ -66,7 +68,7 @@ pub struct ListContext<'list> {
/// Post to be considered by the list's
/// [`PostFilter`](message_filters::PostFilter) stack.
pub struct Post {
pub struct PostEntry {
/// `From` address of post.
pub from: Address,
/// Raw bytes of post.
@ -78,9 +80,9 @@ pub struct Post {
pub action: PostAction,
}
impl core::fmt::Debug for Post {
impl core::fmt::Debug for PostEntry {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
fmt.debug_struct("Post")
fmt.debug_struct(stringify!(PostEntry))
.field("from", &self.from)
.field("bytes", &format_args!("{} bytes", self.bytes.len()))
.field("to", &self.to.as_slice())

View File

@ -38,7 +38,26 @@
//!
//! so the processing stops at the first returned error.
use super::*;
use log::trace;
use melib::Address;
use crate::{
mail::{ListContext, MailJob, PostAction, PostEntry},
models::{DbVal, MailingList},
Connection,
};
impl Connection {
/// Return the post filters of a mailing list.
pub fn list_filters(&self, _list: &DbVal<MailingList>) -> Vec<Box<dyn PostFilter>> {
vec![
Box::new(FixCRLF),
Box::new(PostRightsCheck),
Box::new(AddListHeaders),
Box::new(FinalizeRecipients),
]
}
}
/// Filter that modifies and/or verifies a post candidate. On rejection, return
/// a string describing the error and optionally set `post.action` to `Reject`
@ -49,9 +68,9 @@ pub trait PostFilter {
/// processing to stop and return an `Result::Err`.
fn feed<'p, 'list>(
self: Box<Self>,
post: &'p mut Post,
post: &'p mut PostEntry,
ctx: &'p mut ListContext<'list>,
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()>;
) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()>;
}
/// Check that submitter can post to list, for now it accepts everything.
@ -59,9 +78,9 @@ pub struct PostRightsCheck;
impl PostFilter for PostRightsCheck {
fn feed<'p, 'list>(
self: Box<Self>,
post: &'p mut Post,
post: &'p mut PostEntry,
ctx: &'p mut ListContext<'list>,
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
trace!("Running PostRightsCheck filter");
if let Some(ref policy) = ctx.post_policy {
if policy.announce_only {
@ -117,9 +136,9 @@ pub struct FixCRLF;
impl PostFilter for FixCRLF {
fn feed<'p, 'list>(
self: Box<Self>,
post: &'p mut Post,
post: &'p mut PostEntry,
ctx: &'p mut ListContext<'list>,
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
trace!("Running FixCRLF filter");
use std::io::prelude::*;
let mut new_vec = Vec::with_capacity(post.bytes.len());
@ -137,9 +156,9 @@ pub struct AddListHeaders;
impl PostFilter for AddListHeaders {
fn feed<'p, 'list>(
self: Box<Self>,
post: &'p mut Post,
post: &'p mut PostEntry,
ctx: &'p mut ListContext<'list>,
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
trace!("Running AddListHeaders filter");
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
let sender = format!("<{}>", ctx.list.address);
@ -206,9 +225,9 @@ pub struct ArchivedAtLink;
impl PostFilter for ArchivedAtLink {
fn feed<'p, 'list>(
self: Box<Self>,
post: &'p mut Post,
post: &'p mut PostEntry,
ctx: &'p mut ListContext<'list>,
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
trace!("Running ArchivedAtLink filter");
Ok((post, ctx))
}
@ -220,9 +239,9 @@ pub struct FinalizeRecipients;
impl PostFilter for FinalizeRecipients {
fn feed<'p, 'list>(
self: Box<Self>,
post: &'p mut Post,
post: &'p mut PostEntry,
ctx: &'p mut ListContext<'list>,
) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
trace!("Running FinalizeRecipients filter");
let mut recipients = vec![];
let mut digests = vec![];

View File

@ -17,12 +17,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! How each list handles new posts and new subscriptions.
pub use post_policy::*;
pub use subscription_policy::*;
use super::*;
mod post_policy {
use super::*;
use log::trace;
use rusqlite::OptionalExtension;
use crate::{
errors::{ErrorKind::*, *},
models::{DbVal, PostPolicy},
Connection,
};
impl Connection {
/// Fetch the post policy of a mailing list.
@ -207,7 +215,14 @@ mod post_policy {
}
mod subscription_policy {
use super::*;
use log::trace;
use rusqlite::OptionalExtension;
use crate::{
errors::{ErrorKind::*, *},
models::{DbVal, SubscriptionPolicy},
Connection,
};
impl Connection {
/// Fetch the subscription policy of a mailing list.

View File

@ -17,10 +17,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Processing new posts.
use std::borrow::Cow;
use super::*;
use crate::mail::ListRequest;
use log::{info, trace};
use melib::Envelope;
use rusqlite::OptionalExtension;
use crate::{
errors::*,
mail::{ListContext, ListRequest, PostAction, PostEntry},
models::{changesets::AccountChangeset, Account, DbVal, ListSubscription, MailingList, Post},
queue::{Queue, QueueEntry},
templates::Template,
Connection,
};
impl Connection {
/// Insert a mailing list post into the database.
@ -155,7 +167,6 @@ impl Connection {
}
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);
@ -170,7 +181,7 @@ impl Connection {
subscriptions: &subscriptions,
scheduled_jobs: vec![],
};
let mut post = Post {
let mut post = PostEntry {
from: env.from()[0].clone(),
bytes: raw.to_vec(),
to: env.to().to_vec(),
@ -183,7 +194,7 @@ impl Connection {
});
trace!("result {:#?}", result);
let Post { bytes, action, .. } = post;
let PostEntry { bytes, action, .. } = post;
trace!("Action is {:#?}", action);
let post_env = melib::Envelope::from_bytes(&bytes, None)?;
match action {

View File

@ -21,7 +21,9 @@
use std::borrow::Cow;
use super::*;
use melib::Envelope;
use crate::{errors::*, models::DbVal, Connection, DateTime};
/// In-database queues of mail.
#[derive(Copy, Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
@ -262,6 +264,7 @@ impl Connection {
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
#[test]
fn test_queue_delete_array() {

View File

@ -23,7 +23,7 @@ use std::{future::Future, pin::Pin};
use melib::smtp::*;
use crate::{errors::*, Connection, QueueEntry};
use crate::{errors::*, queue::QueueEntry, Connection};
type ResultFuture<T> = Result<Pin<Box<dyn Future<Output = Result<T>> + Send + 'static>>>;

View File

@ -17,7 +17,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use super::*;
//! User subscriptions.
use log::trace;
use rusqlite::OptionalExtension;
use crate::{
errors::{ErrorKind::*, *},
models::{
changesets::{AccountChangeset, ListSubscriptionChangeset},
Account, ListCandidateSubscription, ListSubscription,
},
Connection, DbVal,
};
impl Connection {
/// Fetch all subscriptions of a mailing list.
@ -589,6 +601,7 @@ impl Connection {
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
#[test]
fn test_subscription_ops() {

View File

@ -17,9 +17,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Named templates, for generated e-mail like confirmations, alerts etc.
//!
//! Template database model: [`Template`].
use super::*;
use log::trace;
use rusqlite::OptionalExtension;
use crate::{
errors::{ErrorKind::*, *},
Connection, DbVal,
};
/// A named template.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
@ -202,3 +210,159 @@ impl Template {
}
}
}
impl Connection {
/// Fetch all.
pub fn fetch_templates(&self) -> Result<Vec<DbVal<Template>>> {
let mut stmt = self
.connection
.prepare("SELECT * FROM templates ORDER BY pk;")?;
let iter = stmt.query_map(rusqlite::params![], |row| {
let pk = row.get("pk")?;
Ok(DbVal(
Template {
pk,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
},
pk,
))
})?;
let mut ret = vec![];
for templ in iter {
let templ = templ?;
ret.push(templ);
}
Ok(ret)
}
/// Fetch a named template.
pub fn fetch_template(
&self,
template: &str,
list_pk: Option<i64>,
) -> Result<Option<DbVal<Template>>> {
let mut stmt = self
.connection
.prepare("SELECT * FROM templates WHERE name = ? AND list IS ?;")?;
let ret = stmt
.query_row(rusqlite::params![&template, &list_pk], |row| {
let pk = row.get("pk")?;
Ok(DbVal(
Template {
pk,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
},
pk,
))
})
.optional()?;
if ret.is_none() && list_pk.is_some() {
let mut stmt = self
.connection
.prepare("SELECT * FROM templates WHERE name = ? AND list IS NULL;")?;
Ok(stmt
.query_row(rusqlite::params![&template], |row| {
let pk = row.get("pk")?;
Ok(DbVal(
Template {
pk,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
},
pk,
))
})
.optional()?)
} else {
Ok(ret)
}
}
/// Insert a named template.
pub fn add_template(&self, template: Template) -> Result<DbVal<Template>> {
let mut stmt = self.connection.prepare(
"INSERT INTO templates(name, list, subject, headers_json, body) VALUES(?, ?, ?, ?, ?) \
RETURNING *;",
)?;
let ret = stmt
.query_row(
rusqlite::params![
&template.name,
&template.list,
&template.subject,
&template.headers_json,
&template.body
],
|row| {
let pk = row.get("pk")?;
Ok(DbVal(
Template {
pk,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
},
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_template {:?}.", &ret);
Ok(ret)
}
/// Remove a named template.
pub fn remove_template(&self, template: &str, list_pk: Option<i64>) -> Result<Template> {
let mut stmt = self
.connection
.prepare("DELETE FROM templates WHERE name = ? AND list IS ? RETURNING *;")?;
let ret = stmt.query_row(rusqlite::params![&template, &list_pk], |row| {
Ok(Template {
pk: -1,
name: row.get("name")?,
list: row.get("list")?,
subject: row.get("subject")?,
headers_json: row.get("headers_json")?,
body: row.get("body")?,
})
})?;
trace!(
"remove_template {} list_pk {:?} {:?}.",
template,
&list_pk,
&ret
);
Ok(ret)
}
}

View File

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use mailpot::{models::*, Configuration, Connection, Queue, SendMail};
use mailpot::{models::*, queue::Queue, Configuration, Connection, SendMail};
use mailpot_tests::init_stderr_logging;
use tempfile::TempDir;

View File

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use mailpot::{melib, models::*, Configuration, Connection, Queue, SendMail};
use mailpot::{melib, models::*, queue::Queue, Configuration, Connection, SendMail};
use mailpot_tests::init_stderr_logging;
use tempfile::TempDir;

View File

@ -18,7 +18,7 @@
*/
use log::{trace, warn};
use mailpot::{melib, models::*, Configuration, Connection, Queue, SendMail};
use mailpot::{melib, models::*, queue::Queue, Configuration, Connection, SendMail};
use mailpot_tests::*;
use melib::smol;
use tempfile::TempDir;

View File

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use mailpot::{models::*, Configuration, Connection, Queue, SendMail};
use mailpot::{models::*, queue::Queue, Configuration, Connection, SendMail};
use mailpot_tests::init_stderr_logging;
use tempfile::TempDir;

View File

@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use mailpot::{models::*, Configuration, Connection, Queue, SendMail, Template};
use mailpot::{models::*, queue::Queue, Configuration, Connection, SendMail, Template};
use mailpot_tests::init_stderr_logging;
use tempfile::TempDir;

View File

@ -94,7 +94,13 @@ See <https://www.postfix.org/master.5.html>.
.br
mpot sample\-config [\-\-with\-smtp \fIWITH_SMTP\fR]
.br
Prints a sample config file to STDOUT.
.TP
\-\-with\-smtp
Use an SMTP connection instead of a shell process.
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\fB