From ccc9b6abb66ebda0b91b4e21f8ec2fb2e87390f7 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 27 Dec 2023 18:33:06 +0100 Subject: add a --dev mode --- src/config.rs | 1 + src/login/demo_provider.rs | 48 +++++++++ src/login/mod.rs | 1 + src/main.rs | 26 ++++- src/server.rs | 3 +- tests/README.md | 38 -------- tests/docker-compose.yml | 22 ----- tests/docker/cyrus/Dockerfile | 15 --- tests/docker/cyrus/entrypoint.sh | 15 --- tests/docker/maddy/Dockerfile | 16 --- tests/docker/maddy/entrypoint.sh | 9 -- tests/inject_emails.sh | 21 ---- tests/instrumentation/README.md | 38 ++++++++ tests/instrumentation/docker-compose.yml | 22 +++++ tests/instrumentation/docker/cyrus/Dockerfile | 15 +++ tests/instrumentation/docker/cyrus/entrypoint.sh | 15 +++ tests/instrumentation/docker/maddy/Dockerfile | 16 +++ tests/instrumentation/docker/maddy/entrypoint.sh | 9 ++ tests/instrumentation/inject_emails.sh | 21 ++++ .../rm-mail-parser-expected-struct.py | 17 ++++ tests/instrumentation/send-to-imap.py | 108 +++++++++++++++++++++ tests/instrumentation/unix2dos.py | 15 +++ tests/rm-mail-parser-expected-struct.py | 17 ---- tests/send-to-imap.py | 108 --------------------- tests/unix2dos.py | 15 --- 25 files changed, 351 insertions(+), 280 deletions(-) create mode 100644 src/login/demo_provider.rs delete mode 100644 tests/README.md delete mode 100644 tests/docker-compose.yml delete mode 100644 tests/docker/cyrus/Dockerfile delete mode 100755 tests/docker/cyrus/entrypoint.sh delete mode 100644 tests/docker/maddy/Dockerfile delete mode 100755 tests/docker/maddy/entrypoint.sh delete mode 100755 tests/inject_emails.sh create mode 100644 tests/instrumentation/README.md create mode 100644 tests/instrumentation/docker-compose.yml create mode 100644 tests/instrumentation/docker/cyrus/Dockerfile create mode 100755 tests/instrumentation/docker/cyrus/entrypoint.sh create mode 100644 tests/instrumentation/docker/maddy/Dockerfile create mode 100755 tests/instrumentation/docker/maddy/entrypoint.sh create mode 100755 tests/instrumentation/inject_emails.sh create mode 100644 tests/instrumentation/rm-mail-parser-expected-struct.py create mode 100644 tests/instrumentation/send-to-imap.py create mode 100755 tests/instrumentation/unix2dos.py delete mode 100644 tests/rm-mail-parser-expected-struct.py delete mode 100644 tests/send-to-imap.py delete mode 100755 tests/unix2dos.py diff --git a/src/config.rs b/src/config.rs index 1438910..b9c1f09 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,6 +26,7 @@ pub struct ProviderConfig { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "user_driver")] pub enum UserManagement { + Demo, Static(LoginStaticConfig), Ldap(LoginLdapConfig), } diff --git a/src/login/demo_provider.rs b/src/login/demo_provider.rs new file mode 100644 index 0000000..0efb37c --- /dev/null +++ b/src/login/demo_provider.rs @@ -0,0 +1,48 @@ +use crate::login::*; +use crate::storage::*; + +pub struct DemoLoginProvider{ + keys: CryptoKeys, + in_memory_store: in_memory::MemDb, +} + +impl DemoLoginProvider { + pub fn new() -> Self { + Self { + keys: CryptoKeys::init(), + in_memory_store: in_memory::MemDb::new(), + } + } +} + +#[async_trait] +impl LoginProvider for DemoLoginProvider { + async fn login(&self, username: &str, password: &str) -> Result { + tracing::debug!(user=%username, "login"); + + if username != "alice" { + bail!("user does not exist"); + } + + if password != "hunter2" { + bail!("wrong password"); + } + + let storage = self.in_memory_store.builder("alice").await; + let keys = self.keys.clone(); + + Ok(Credentials { storage, keys }) + } + + async fn public_login(&self, email: &str) -> Result { + tracing::debug!(user=%email, "public_login"); + if email != "alice@example.tld" { + bail!("invalid email address"); + } + + let storage = self.in_memory_store.builder("alice").await; + let public_key = self.keys.public.clone(); + + Ok(PublicCredentials { storage, public_key }) + } +} diff --git a/src/login/mod.rs b/src/login/mod.rs index 2926738..6f2ca31 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,5 +1,6 @@ pub mod ldap_provider; pub mod static_provider; +pub mod demo_provider; use base64::Engine; use std::sync::Arc; diff --git a/src/main.rs b/src/main.rs index 3221c2e..3baa8e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,12 @@ struct Args { #[clap(subcommand)] command: Command, + /// A special mode dedicated to developers, NOT INTENDED FOR PRODUCTION + #[clap(long)] + dev: bool, + #[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")] + /// Path to the main Aerogramme configuration file config_file: PathBuf, } @@ -158,7 +163,22 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let any_config = read_config(args.config_file)?; + let any_config = if args.dev { + use std::net::*; + AnyConfig::Provider(ProviderConfig { + pid: None, + imap: ImapConfig { + bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143), + }, + lmtp: LmtpConfig { + bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025), + hostname: "example.tld".to_string(), + }, + users: UserManagement::Demo, + }) + } else { + read_config(args.config_file)? + }; match (&args.command, any_config) { (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand { @@ -184,8 +204,8 @@ async fn main() -> Result<()> { ProviderCommand::Account(cmd) => { let user_file = match config.users { UserManagement::Static(conf) => conf.user_list, - UserManagement::Ldap(_) => { - panic!("LDAP account management is not supported from Aerogramme.") + _ => { + panic!("Only static account management is supported from Aerogramme.") } }; account_management(&args.command, cmd, user_file)?; diff --git a/src/server.rs b/src/server.rs index 28e0b27..1b8677b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -11,7 +11,7 @@ use crate::config::*; use crate::imap; use crate::lmtp::*; use crate::login::ArcLoginProvider; -use crate::login::{ldap_provider::*, static_provider::*}; +use crate::login::{ldap_provider::*, static_provider::*, demo_provider::*}; pub struct Server { lmtp_server: Option>, @@ -36,6 +36,7 @@ impl Server { pub async fn from_provider_config(config: ProviderConfig) -> Result { tracing::info!("Init as provider"); let login: ArcLoginProvider = match config.users { + UserManagement::Demo => Arc::new(DemoLoginProvider::new()), UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?), UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?), }; diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index b479cf4..0000000 --- a/tests/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Spawn Dovecot+Maddy+Cyrus - -Run: - -``` -docker-compose up -``` - - - Dovecot - - listen on :993, run `openssl s_client -connect 127.0.0.1:993` - - login with `A LOGIN test pass` - - Maddy - - listen on :994, run `openssl s_client -connect 127.0.0.1:994` - - login with `A LOGIN test@example.com pass` - - Cyrus - - listen on :143, run `nc 127.0.0.1 143` - - login with `A LOGIN test pass` - - Stalwart - - listen on :1993, run `openssl s_client -connect 127.0.0.1:993` - - login with `A LOGIN test@example.com pass` - - note: not packaged in docker yet... - -Other IMAP servers we could add: - - WildDuck (own node.js imap implementation) - - https://github.com/nodemailer/wildduck - - DBMail (own C IMAP implementation) - - https://github.com/dbmail/dbmail/tree/master - - UW IMAP (known to be the reference IMAP implementation) - - https://wiki.archlinux.org/title/UW_IMAP - - Apache James (has its own implementation of IMAP too) - - https://james.apache.org/ - - Citadel - - https://citadel.org - - https://code.citadel.org/?p=citadel;a=tree;f=citadel/server/modules/imap;h=3ceaa1d6b518bddb7539911a8dd9d81136d4e594;hb=HEAD - -# Inject emails and dump the computed `BODY` + `BODYSTRUCTURE` - -Once you ran `docker-compose up`, launch `./send-to-imap.py` diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml deleted file mode 100644 index 6a112bb..0000000 --- a/tests/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.4' -services: - dovecot: - image: dovecot/dovecot:2.3.19.1 - ports: - - "993:993/tcp" - - maddy: - build: - context: ./docker/maddy/ - image: maddy - ports: - - "994:993/tcp" - - cyrus: - build: - context: ./docker/cyrus/ - image: cyrus - volumes: - - "/dev/log:/dev/log" - ports: - - "143:143/tcp" diff --git a/tests/docker/cyrus/Dockerfile b/tests/docker/cyrus/Dockerfile deleted file mode 100644 index 67b4e11..0000000 --- a/tests/docker/cyrus/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM debian:buster - -ARG DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y cyrus-imapd cyrus-pop3d cyrus-nntpd cyrus-caldav cyrus-admin sasl2-bin && \ - echo "admins: cyrus" >> /etc/imapd.conf && \ - touch /var/lib/cyrus/tls_sessions.db && \ - chown cyrus:mail /var/lib/cyrus/tls_sessions.db && \ - mkdir /run/cyrus && \ - chown -R cyrus:mail /run/cyrus - -COPY entrypoint.sh /usr/local/bin/entrypoint.sh - -CMD [ "/usr/local/bin/entrypoint.sh" ] - diff --git a/tests/docker/cyrus/entrypoint.sh b/tests/docker/cyrus/entrypoint.sh deleted file mode 100755 index c410e9d..0000000 --- a/tests/docker/cyrus/entrypoint.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -cyrmaster -D -l 32 -C /etc/imapd.conf -M /etc/cyrus.conf & -sleep 2 - -echo cyrus | saslpasswd2 -p cyrus -echo pass | saslpasswd2 -p test - -cyradm -u cyrus -w cyrus 127.0.0.1 <\r" - echo -e "RCPT TO: $USER\r" - echo -e "DATA\r" - cat $mail - echo -e "\r" - echo -e ".\r" - done - echo -e "QUIT\r" -) - -mail_lmtp_session | tee >(nc localhost 12024) diff --git a/tests/instrumentation/README.md b/tests/instrumentation/README.md new file mode 100644 index 0000000..b479cf4 --- /dev/null +++ b/tests/instrumentation/README.md @@ -0,0 +1,38 @@ +# Spawn Dovecot+Maddy+Cyrus + +Run: + +``` +docker-compose up +``` + + - Dovecot + - listen on :993, run `openssl s_client -connect 127.0.0.1:993` + - login with `A LOGIN test pass` + - Maddy + - listen on :994, run `openssl s_client -connect 127.0.0.1:994` + - login with `A LOGIN test@example.com pass` + - Cyrus + - listen on :143, run `nc 127.0.0.1 143` + - login with `A LOGIN test pass` + - Stalwart + - listen on :1993, run `openssl s_client -connect 127.0.0.1:993` + - login with `A LOGIN test@example.com pass` + - note: not packaged in docker yet... + +Other IMAP servers we could add: + - WildDuck (own node.js imap implementation) + - https://github.com/nodemailer/wildduck + - DBMail (own C IMAP implementation) + - https://github.com/dbmail/dbmail/tree/master + - UW IMAP (known to be the reference IMAP implementation) + - https://wiki.archlinux.org/title/UW_IMAP + - Apache James (has its own implementation of IMAP too) + - https://james.apache.org/ + - Citadel + - https://citadel.org + - https://code.citadel.org/?p=citadel;a=tree;f=citadel/server/modules/imap;h=3ceaa1d6b518bddb7539911a8dd9d81136d4e594;hb=HEAD + +# Inject emails and dump the computed `BODY` + `BODYSTRUCTURE` + +Once you ran `docker-compose up`, launch `./send-to-imap.py` diff --git a/tests/instrumentation/docker-compose.yml b/tests/instrumentation/docker-compose.yml new file mode 100644 index 0000000..6a112bb --- /dev/null +++ b/tests/instrumentation/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.4' +services: + dovecot: + image: dovecot/dovecot:2.3.19.1 + ports: + - "993:993/tcp" + + maddy: + build: + context: ./docker/maddy/ + image: maddy + ports: + - "994:993/tcp" + + cyrus: + build: + context: ./docker/cyrus/ + image: cyrus + volumes: + - "/dev/log:/dev/log" + ports: + - "143:143/tcp" diff --git a/tests/instrumentation/docker/cyrus/Dockerfile b/tests/instrumentation/docker/cyrus/Dockerfile new file mode 100644 index 0000000..67b4e11 --- /dev/null +++ b/tests/instrumentation/docker/cyrus/Dockerfile @@ -0,0 +1,15 @@ +FROM debian:buster + +ARG DEBIAN_FRONTEND=noninteractive +RUN apt update && \ + apt install -y cyrus-imapd cyrus-pop3d cyrus-nntpd cyrus-caldav cyrus-admin sasl2-bin && \ + echo "admins: cyrus" >> /etc/imapd.conf && \ + touch /var/lib/cyrus/tls_sessions.db && \ + chown cyrus:mail /var/lib/cyrus/tls_sessions.db && \ + mkdir /run/cyrus && \ + chown -R cyrus:mail /run/cyrus + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh + +CMD [ "/usr/local/bin/entrypoint.sh" ] + diff --git a/tests/instrumentation/docker/cyrus/entrypoint.sh b/tests/instrumentation/docker/cyrus/entrypoint.sh new file mode 100755 index 0000000..c410e9d --- /dev/null +++ b/tests/instrumentation/docker/cyrus/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +cyrmaster -D -l 32 -C /etc/imapd.conf -M /etc/cyrus.conf & +sleep 2 + +echo cyrus | saslpasswd2 -p cyrus +echo pass | saslpasswd2 -p test + +cyradm -u cyrus -w cyrus 127.0.0.1 <\r" + echo -e "RCPT TO: $USER\r" + echo -e "DATA\r" + cat $mail + echo -e "\r" + echo -e ".\r" + done + echo -e "QUIT\r" +) + +mail_lmtp_session | tee >(nc localhost 12024) diff --git a/tests/instrumentation/rm-mail-parser-expected-struct.py b/tests/instrumentation/rm-mail-parser-expected-struct.py new file mode 100644 index 0000000..3dbc56e --- /dev/null +++ b/tests/instrumentation/rm-mail-parser-expected-struct.py @@ -0,0 +1,17 @@ +from os import listdir +from os.path import isfile, join +import sys + +path = sys.argv[1] +onlyfiles = [join(path, f) for f in listdir(path) if isfile(join(path, f)) and len(f) > 4 and f[-4:] == ".txt"] + +for p in onlyfiles: + g = p[:-4] + ".eml" + print(f"{p} -> {g}") + with open(p, 'r+b') as inp: + with open(g, 'w+b') as out: + for line in inp: + if b"EXPECTED STRUCTURE" in line: + break + out.write(line) + diff --git a/tests/instrumentation/send-to-imap.py b/tests/instrumentation/send-to-imap.py new file mode 100644 index 0000000..df49076 --- /dev/null +++ b/tests/instrumentation/send-to-imap.py @@ -0,0 +1,108 @@ +from imaplib import IMAP4_SSL, IMAP4 +from os import listdir +from os.path import isfile, join +import sys + +# COMMAND USAGE +# +# start a test IMAP servers: +# docker-compose.up +# then call this script. eg: +# ./send-to-imap.py all ./emails/dxflrs/ + + +def rebuild_body_res(b): + bb = b'' + for e in b: + if type(e) is tuple: + bb += b'\r\n'.join([p for p in e]) + else: + bb += e + + f = bb[bb.find(b'('):] + return f + +mode = sys.argv[1] +path = sys.argv[2] + +base_test_mb = "kzUXL7HyS5OjLcU8" +parameters = { + "dovecot": { + "con": IMAP4_SSL, + "port": 993, + "user": "test", + "pw": "pass", + "ext": ".dovecot", + "mb": base_test_mb, + }, + "maddy": { + "con": IMAP4_SSL, + "port": 994, + "user": "test@example.com", + "pw": "pass", + "ext": ".maddy", + "mb": base_test_mb, + }, + "cyrus": { + "con": IMAP4, + "port": 143, + "user": "test", + "pw": "pass", + "ext": ".cyrus", + "mb": "INBOX."+base_test_mb, + }, + "stalwart": { + "con": IMAP4_SSL, + "port": 1993, + "user": "test@example.com", + "pw": "pass", + "ext": ".stalwart.0.2.0", + "mb": base_test_mb, + } +} + +queue = list(parameters.keys()) +if mode in parameters: + queue = [ mode ] + +onlyfiles = [join(path, f) for f in listdir(path) if isfile(join(path, f)) and len(f) > 4 and f[-4:] == ".eml"] + +for target in queue: + print(f"--- {target} ---") + conf = parameters[target] + test_mb = conf['mb'] + + with conf['con'](host="localhost", port=conf['port']) as M: + print(M.login(conf['user'], conf['pw'])) + print(M.delete(test_mb)) + print(M.create(test_mb)) + + + print(M.list()) + print(M.select(test_mb)) + failed = 0 + for (idx, f) in enumerate(onlyfiles): + f_noext = f[:-4] + try: + with open(f, 'r+b') as mail: + print(M.append(test_mb, [], None, mail.read())) + seq = (f"{idx+1-failed}:{idx+1-failed}").encode() + (r, b) = M.fetch(seq, "(BODY)") + print((r, b)) + assert r == 'OK' + + + with open(f_noext + conf['ext'] + ".body", 'w+b') as w: + w.write(rebuild_body_res(b)) + + (r, b) = M.fetch(seq, "(BODYSTRUCTURE)") + print((r, b)) + assert r == 'OK' + with open(f_noext + conf['ext'] + ".bodystructure", 'w+b') as w: + w.write(rebuild_body_res(b)) + except: + failed += 1 + print(f"failed {f}") + + M.close() + M.logout() diff --git a/tests/instrumentation/unix2dos.py b/tests/instrumentation/unix2dos.py new file mode 100755 index 0000000..4e6c9cf --- /dev/null +++ b/tests/instrumentation/unix2dos.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +import sys + +buf = "" +with open(sys.argv[1], 'r+b') as f: + buf = f.read() + +if buf.find(b'\r\n'): + print(f"{sys.argv[1]} is already a CRLF file") + sys.exit(1) + +buf = buf.replace(b'\n', b'\r\n') + +with open(sys.argv[1], 'w+b') as f: + f.write(buf) diff --git a/tests/rm-mail-parser-expected-struct.py b/tests/rm-mail-parser-expected-struct.py deleted file mode 100644 index 3dbc56e..0000000 --- a/tests/rm-mail-parser-expected-struct.py +++ /dev/null @@ -1,17 +0,0 @@ -from os import listdir -from os.path import isfile, join -import sys - -path = sys.argv[1] -onlyfiles = [join(path, f) for f in listdir(path) if isfile(join(path, f)) and len(f) > 4 and f[-4:] == ".txt"] - -for p in onlyfiles: - g = p[:-4] + ".eml" - print(f"{p} -> {g}") - with open(p, 'r+b') as inp: - with open(g, 'w+b') as out: - for line in inp: - if b"EXPECTED STRUCTURE" in line: - break - out.write(line) - diff --git a/tests/send-to-imap.py b/tests/send-to-imap.py deleted file mode 100644 index df49076..0000000 --- a/tests/send-to-imap.py +++ /dev/null @@ -1,108 +0,0 @@ -from imaplib import IMAP4_SSL, IMAP4 -from os import listdir -from os.path import isfile, join -import sys - -# COMMAND USAGE -# -# start a test IMAP servers: -# docker-compose.up -# then call this script. eg: -# ./send-to-imap.py all ./emails/dxflrs/ - - -def rebuild_body_res(b): - bb = b'' - for e in b: - if type(e) is tuple: - bb += b'\r\n'.join([p for p in e]) - else: - bb += e - - f = bb[bb.find(b'('):] - return f - -mode = sys.argv[1] -path = sys.argv[2] - -base_test_mb = "kzUXL7HyS5OjLcU8" -parameters = { - "dovecot": { - "con": IMAP4_SSL, - "port": 993, - "user": "test", - "pw": "pass", - "ext": ".dovecot", - "mb": base_test_mb, - }, - "maddy": { - "con": IMAP4_SSL, - "port": 994, - "user": "test@example.com", - "pw": "pass", - "ext": ".maddy", - "mb": base_test_mb, - }, - "cyrus": { - "con": IMAP4, - "port": 143, - "user": "test", - "pw": "pass", - "ext": ".cyrus", - "mb": "INBOX."+base_test_mb, - }, - "stalwart": { - "con": IMAP4_SSL, - "port": 1993, - "user": "test@example.com", - "pw": "pass", - "ext": ".stalwart.0.2.0", - "mb": base_test_mb, - } -} - -queue = list(parameters.keys()) -if mode in parameters: - queue = [ mode ] - -onlyfiles = [join(path, f) for f in listdir(path) if isfile(join(path, f)) and len(f) > 4 and f[-4:] == ".eml"] - -for target in queue: - print(f"--- {target} ---") - conf = parameters[target] - test_mb = conf['mb'] - - with conf['con'](host="localhost", port=conf['port']) as M: - print(M.login(conf['user'], conf['pw'])) - print(M.delete(test_mb)) - print(M.create(test_mb)) - - - print(M.list()) - print(M.select(test_mb)) - failed = 0 - for (idx, f) in enumerate(onlyfiles): - f_noext = f[:-4] - try: - with open(f, 'r+b') as mail: - print(M.append(test_mb, [], None, mail.read())) - seq = (f"{idx+1-failed}:{idx+1-failed}").encode() - (r, b) = M.fetch(seq, "(BODY)") - print((r, b)) - assert r == 'OK' - - - with open(f_noext + conf['ext'] + ".body", 'w+b') as w: - w.write(rebuild_body_res(b)) - - (r, b) = M.fetch(seq, "(BODYSTRUCTURE)") - print((r, b)) - assert r == 'OK' - with open(f_noext + conf['ext'] + ".bodystructure", 'w+b') as w: - w.write(rebuild_body_res(b)) - except: - failed += 1 - print(f"failed {f}") - - M.close() - M.logout() diff --git a/tests/unix2dos.py b/tests/unix2dos.py deleted file mode 100755 index 4e6c9cf..0000000 --- a/tests/unix2dos.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -import sys - -buf = "" -with open(sys.argv[1], 'r+b') as f: - buf = f.read() - -if buf.find(b'\r\n'): - print(f"{sys.argv[1]} is already a CRLF file") - sys.exit(1) - -buf = buf.replace(b'\n', b'\r\n') - -with open(sys.argv[1], 'w+b') as f: - f.write(buf) -- cgit v1.2.3