MiniMail API

this is a minimal maildir REST API, based on busybox httpd and 75 lines of shell script

it is useable on mailservers with postfix

Reference Guide

 Version: 0.5
 Date: So 6. Okt 19:34:46 CEST 2024
 Author: reinhard@finalmedia.de

 # What is maildir?

 See here for further information.

 # What is an API (generally)?

 An Application Programming Interface is an interface exposed by a program.
 Programs that use the API can use the features of the program that exposes the API. 
 APIs are only used by programs, they are not user interfaces. 
 
 # What does this API do?

 This API uses the existing maildir of your postfix. you choose which specific maildirs
 get exposed with the API, just by symlinking them in a special folder /etc/apitokens/. 
 The Name of the symlink **is** the token. So you can define multiple tokens to the same mailbox,
 or even change tokens frequently (e.g. with additional totp tokens).

 Then you can use http requests to query files out of the maildir. you can also
 store some additional draft files there, just by using multiple http requests and your token.  

 # API Usage
 ## Reading Mailbox (METHOD: GET)

 ./cgi-bin/v1/get/YOURTOKEN/*/* ............................ list all mailbox folders as json array
 ./cgi-bin/v1/get/YOURTOKEN/cur/* .......................... list all mails in cur inbox as json array
 ./cgi-bin/v1/get/YOURTOKEN/api/* .......................... list all your api mail drafts as json array
 ./cgi-bin/v1/get/YOURTOKEN/.foldername/* .................. list all mails in given (sub)folder as json array
 ./cgi-bin/v1/get/YOURTOKEN/cur/filename ................... output content of the mail file (you can request parts of the filename. auto complete function)
 ./cgi-bin/v1/get/YOURTOKEN/.foldername/filename ........... dito. but in subfolder.
 ./cgi-bin/v1/get/YOURTOKEN/api/draftfilenumber............. output content of you current draf, specified by draftfilenumber. (also concat and auto complete available)
 ./cgi-bin/v1/get/YOURTOKEN/*/subscriptions ................ output plain content of your imap client subscriptions file, if available (dovecot at the same server)
 ./cgi-bin/v1/get/YOURTOKEN/*/dovecot-keywords ............. output plain content of your dovecot keywords file, if available (dovecot at the same server)

 ## Adding Data Chunks to mail compose/draft file (METHOD: POST)

 ./cgi-bin/v1/add/YOURTOKEN/draftnumber/hex_sequence........ writing binary content in your specified draftfile in mailbox folder "api". submit your binary content hex encoded as ascii chars 0-9a-f sequence as path. (max 1024 hexchars per request and sequence)

 	Falls das draftfile verzeichnis api noch nicht existiert, wird es automatisch angelegt. es liegt in der mailbox auf der gleichen ebene wie cur, new und tmp
 	Ein draftfile name darf nur nummerisch(!) sein. z.B. 12345789 
 	Wenn die draftfile datei noch nicht existiert, wird sie angelegt. existiert die datei, wird der gewuenschte content angehaengt.
 	Dabei wird der content in der URL hexadezimal codiert gesetzt und serverseitig decodiert in die datei geschrieben.

 	Beachte: Die Maximale Laenge der gesamten URL wird auf 4096 zeichen begrenzt. werden mehr zeichen uebergeben, wird abgeschnitten.
	Es obliegt dem client, dieses Limit nicht zu überschreiten. 
	In das Zeichenlimit wird auch der Stammpfad "/cgi-bin/v1/add/YOURTOKEN/draftfilenumber/" mit eingerechnet.
	Und die Zeichen deiner payload sind in hexchars also pro byte jeweils 2 Zeichen. 
 	Du musst also z.B. fuer "hello world" mit 22 Zeichen rechnen: 68656c6c6f20776f726c64 
 	und auch ein newline musst du beruecksichtigen, falls du diesen hinzufuegen willst (0a).

 	Es werden daher maximal 1024 chars pro request und hex sequence empfohlen. 

	Warum ist das so realisiert und warum kann ich nicht die komplette payload einfach in einem json object bei einem request übergeben?

	Auf diesen Weise kann man Transaktionen besser sicherstellen. Würde die ganze Payload einer Email auf einmal übertragen, ist
	Ein Datenverlust wahrscheinlicher und es müssten auf Serverseite auch deutlich mehr Ressourcen vorgehalten werden, die nicht gut kalkulierbar sein
	und nicht durch ein ratelimit beinflusst werden könnten.  Auch hilft ein Chunking der Payload bei einer potentiellen Outage auf Serverseite. 
	Bei einer Outage kann der Client nämlich den bisherigen Content anfragen und dann den Transfer fortsetzen. Daher wurde der Schreibprozess
	auf diese Weise realisert. Es können also einzelne Chunks übertragen werden und
	auch die Chunksize von der clientseite flexibel gewählt werden: zwischen 1-1024 bytes pro request. 
	Durch die Rückmeldung über Statuscode 200 und 201 kann der client auch den erfolgreichen Storage des jeweiligen chunks sicherstellen oder
	ihn bei Bedarf neu übertragen, statt die gesamte Payload erneut übertragen zu müssen. Auf Serverseite kann ein Chunk dabei im Cache gehalten werden,
	bevor er faktisch atomar gesichert wird. Der Gesamtoverhead ist natürlich höher, weil hier auch einzelne HTTP Requests stattfinden, inkl. dem TCP Overhead.
	Mit dem HTTP/3.0 wäre aber auch ein Transfer via UDP möglich. 

 ## Completly Remove a compose file (METHOD: DELETE)

 ./cgi-bin/v1/del/YOURTOKEN/draftfilenumber.................. remove specified draftfile, if existing in api folder.

 ## Sending composed mailfile (METHOD: POST)

 ./cgi-bin/v1/send/YOURTOKEN/draftfilenumber................. forwarding specified draftfile to postfix sendmail. no further validation.


 	Wenn der Versand in Ordnung war, wird wird mit statuscode 200 geantwortet. 
 	Die draftfile/composefile wird nach dem versand nicht entfernt. Man kann sie also bei bedarf bequem mehrfach versenden. 
 	um sie zu entfernen ist daher explizit der separate del befehl zu benutzen. 

	es kann bei bedarf serverseitig ein globales automatisches "verfallsdatum" für alte draft files gesetzt werden,
	wenn diese länger nicht verändert wurden. Dies ist aber nicht über die API änderbar.

 ### Beispielversand

 Beispielweise hast du das Token 868e2197fb004325 und möchtest nun einen Mail Text in die neue draftfilenumer 123 adden,
 anschließend testweise zur Kontrolle auslesen, dann diese Nachricht viermal versenden und das draftfile wieder entfernen.

 um den text in curl commands umzuwandeln, kann man z.B. in linux diese Pipeline verwenden,
 dabei waehlen wir eine chunksize von 120 chars als Beispiel:


 export YOURTOKEN="868e2197fb004325"
 export DRAFTNUMBER="123"

 cat << ::EOF:: | xxd -ps | tr -dc "0-9a-f" | fold -w 120 | sed "s|^|curl -X POST http://127.0.0.1:5555/cgi-bin/v1/add/${YOURTOKEN}/${DRAFTNUMBER}/|g"
 Subject: Ein kleiner Test
 To: =?UTF-8?Q?J=c3=b6rg_Reinhard?= <example@finalmedia.de>
 From: Max Mustermann <max.mustermann@finalmedia.de>
 Date: $(date +'%a, %-d %b %Y %H:%M:%S %z')
 Content-Type: text/plain; charset=utf-8; format=flowed
 Content-Transfer-Encoding: 8bit

 Guten Tag!
 Dies ist ein kleiner öäü Test.

 ::EOF::


 Dann sind dies diese einzelnen Anfragen:


 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/add/868e2197fb004325/123/5375626a6563743a2045696e206b6c65696e657220546573740a546f3a203d3f5554462d383f513f4a3d63333d623672675f5265696e686172643f3d
 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/add/868e2197fb004325/123/203c6578616d706c654066696e616c6d656469612e64653e0a46726f6d3a204d6178204d75737465726d616e6e203c6d61782e6d75737465726d616e
 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/add/868e2197fb004325/123/6e4066696e616c6d656469612e64653e0a446174653a204d6f6e2c203031204a616e20323032342030393a30303a3233202b303130300a436f6e7465
 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/add/868e2197fb004325/123/6e742d547970653a20746578742f706c61696e3b20636861727365743d7574662d383b20666f726d61743d666c6f7765640a436f6e74656e742d5472
 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/add/868e2197fb004325/123/616e736665722d456e636f64696e673a20386269740a0a477574656e20546167210a44696573206973742065696e206b6c65696e657220c3b6c3a4c3
 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/add/868e2197fb004325/123/bc20546573742e0a0a


 du kann an obiger pipe also | sh anhängen, wenn du diese dann in einem rutsch ausführen willst:


 Mit diesen curl Zeilen wiederum kannst du dann im Anschluss prüfen und viermal versenden und aufräumen.

 curl -X GET http://127.0.0.1:5555/cgi-bin/v1/get/868e2197fb004325/api/123

 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/send/868e2197fb004325/123
 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/send/868e2197fb004325/123
 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/send/868e2197fb004325/123
 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/send/868e2197fb004325/123

 curl -X POST http://127.0.0.1:5555/cgi-bin/v1/del/868e2197fb004325/123




 ### Konkatenierung und Autocomplete des filename bei einem read request

 Um mehrere Mails auf einmal auszugeben (konkateniert), kannst du als dateiname einen range verwenden.

 Wenn du also die liste abfragst, erhältst du ja einen json array in dem z.B. solche Dateinamen zurückgeliefert werden

  _____ timestamp            __hostid
 /           _______ ID     /      ___ info      _____ flags nach maildir
 |          /               |     /             /
 |          |               |     |             |
 1668668756.M289355P3876294.mail,S=3656,W=3721:2,Sa

 info starting with "2,": Each character after the comma is an independent flag.

    Flag "P" (passed): the user has resent/forwarded/bounced this message to someone else.
    Flag "R" (replied): the user has replied to this message.
    Flag "S" (seen): the user has viewed this message, though perhaps he didn't read all the way through it.
    Flag "T" (trashed): the user has moved this message to the trash; the trash will be emptied by a later user action.
    Flag "D" (draft): the user considers this message a draft; toggled at user discretion.
    Flag "F" (flagged): user-defined flag; toggled at user discretion. 


 "Why should I use maildir?"
 DJB: "Two words: no locks. An MUA can read and delete messages while new mail is being delivered: each message is stored in a separate file with a unique name, so it isn't affected by operations on other messages. An MUA doesn't have to worry about partially delivered mail: each message is safely written to disk in the tmp subdirectory before it is moved to new."


 Die normalen Dateinamen von mails in einer maildir beginnen also mit einem unixtimestamp.

 Wichtig: Wenn du eine Mail abfragst, frage normalerweise auf einen Dateinamen vor dem Doppelpunkt

 Frage also auf 1668668756.M289355P3876294.mail und du bekommst die passende mail als ausgabe. 

 Hintergrund ist.. wenn jemand anderes parallel per imap Änderungen vornimmt, werden sich Werte
 nach dem : am dateinamen ändern. 2 ist z.b. eine "gelesene" mail. Da du evtl. die Liste
 aber vorher abgefragt hast, würdest du dann keine gelesene mail mehr unter dem namen finden.
 referenziere daher bei deiner Abfrage 1668668756.M289355P3876294.mail.

 Das System ist aber flexibel. Du kannst auch den vollen Namen
 1668668756.M289355P3876294.mail,S=3656,W=3721:2,Sa 
 anfragen, oder sogar nur 1668668756.M289355P387
 wenn es sonst keine weitere mails mit diesem beginnenden Dateinamen gibt. 

 Gibt es mehrere Dateien nach dem angefragten Schema, werden diese automatisch **konkateniert**
 zurückgeliefert. Konkret:

 Du musst also nicht den vollen filename eingeben, sondern z.b. nur den anfang. Beispiel:
 
 http://127.0.0.1:5555/cgi-bin/v1/get/YOURTOKEN/cur/172

 Das gibt dir aus der aktuellen Hauptordner alle Mails aus, deren Dateiname mit 172 beginnt, also
 unix timestamp 172, folglich 1720000000.. und damit alle Mails ab Mi 3. Jul 11:46:40 CEST 2024

 Tipp: Du kannst mit date -d @1720000000 ausgeben lassen, welche unix timestamps, welchem datum entsprechen.
 Um also z.B. aus einer Mailbox alle Emails konkateniert auszugeben, kannst du sogar das hier verwenden
 
 http://127.0.0.1:5555/cgi-bin/v1/get/YOURTOKEN/cur/1
 
 solange also deren unix-timestamp mit 1 beginnt.



 ## Was sind Tokens? Token Erzeugung auf dem Mailserver

 wenn du deine postfix mails z.B. alle in der maildir /var/vmail/domain/usermailboxname/Maildir/  liegen hast,
 dann erstellst du erstmal ein zentrales tokenverzeichnis:

	mkdir /etc/apitokens/
 	chown vmail:vmail /etc/apitokens
 	chmod u=rx,g=rx,o= /etc/apitokens

 der chmod Befehl ist sehr wichtig. Nicht vergessen! Nur root soll in das Verzeichnis schreiben dürfen
 und nur vmail soll dort lesen dürfen.

 und dort dann symlinks zu einer mailbox erstellen. die symlinks dabei kryptischen benennen. das sind die tokens. 
 beispiel: fuer ein token 868e2197fb004325 machst du als root user

	ln -sf /var/vmail/domain/usermailbox/Maildir/ /etc/apitokens/868e2197fb004325

 damit ist alles fertig. beachte. ein token MUSS hexadezimal ascii sein. Also nur Zeichen 0123456789abcdef erlaubt. Nichts anderes!

 Du machst das für alle mailboxen, die du auch auf diese weise exponieren willst.
 jede mailbox bekommt dabei ein von dir definiertes zufallstoken. 
 Aus sicherheitsgründen sollte dieses token mindestens 16 zeichen lang sein, besser mehr.


 ### Wie nutze ich das nun auf serverseite?

 du startest den service dann z.B. als root mit

 exec env -i busybox httpd -u vmail:vmail -f -p 127.0.0.1:5555 -h minimail/ -vv

 und kannst das z.B: mit den daemontools tun. busybox httpd bindet dann loopback
 an den port 5555 und dropped dann aber direkt seine priviledges zum angegeben user 
 vmail und gruppe vmail. alle scripte aus cgi-bin laufen also mit den Rechten von vmail:vmail.

 und es werden lese und schreibrechte im via /etc/apitokens/ versymlinkten zielverzeichnis
 (also der mailbox) benötigt.