├── .gitignore ├── README.md ├── impl ├── README.md ├── client.go ├── mail.go └── server.go ├── roadmap.md └── spec.md /.gitignore: -------------------------------------------------------------------------------- 1 | TODO 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nanomail 2 | 3 | Nanomail is an ultra-lightwight internet mail protocol, inspired by email. 4 | Unlike email, it is designed to be radically simple and easy to host yourself. 5 | 6 | Think of it like [Gemini](https://gemini.circumlunar.space/) for email. 7 | 8 | Read the [spec](spec.md) and/or check out the [reference implementation](./impl) 9 | 10 | HEAVILY WIP. Mostly me sketching out ideas at this point 11 | 12 | On Github for now until I get it self-hosted 13 | -------------------------------------------------------------------------------- /impl/README.md: -------------------------------------------------------------------------------- 1 | # Example implementation of the nanomail protocol 2 | -------------------------------------------------------------------------------- /impl/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type Nanomail struct { 10 | Signature string 11 | From string 12 | To string 13 | SentAt time.Time 14 | ThreadId string 15 | Subject string 16 | Body string 17 | } 18 | 19 | func (n Nanomail) Validate() error { 20 | for _, v := range []string{n.Signature, n.From, n.To, n.Subject} { 21 | if strings.Contains(v, "\n") { 22 | return fmt.Errorf("Field cannot have newline") 23 | } 24 | } 25 | return nil 26 | } 27 | 28 | func (n *Nanomail) Sign() { 29 | n.Signatrue = "" 30 | } 31 | 32 | func (n Nanomail) StringMinusSignature() string { 33 | return fmt.Sprintf("From: %s\nTo: %s\nSubject: %s\n\n%s", 34 | n.From, n.To, n.Subject, n.Body) 35 | } 36 | func (n Nanomail) String() string { 37 | return fmt.Sprintf("Signature :%s\n%s", 38 | n.Signature, n.StringMinusSignature()) 39 | } 40 | 41 | func main() { 42 | // usage: ./this sigfile from@example.com dest@example.com 43 | // Reads message from stdin 44 | } 45 | -------------------------------------------------------------------------------- /impl/mail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // Commands 10 | const Fetch = "FETCH" 11 | const Send = "SEND" 12 | const GetKey = "GETKEY" 13 | 14 | func ParseHeaders(mail io.Reader) map[string]string { 15 | var headers = make(map[string]string) 16 | scanner := bufio.NewScanner(mail) 17 | for scanner.Scan() { 18 | text := scanner.Text() 19 | b, a := strings.Cut(text, ": ") 20 | headers[b] = a 21 | // Break on new line 22 | if text == "" { 23 | continue 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /impl/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | type Server st 9 | 10 | func (s *Server) Serve(l net.Listener) error { 11 | defer l.Close() 12 | 13 | for { 14 | c, err := l.Accept() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | s.setupConn(c) 20 | go s.ServeConn(context.Background(), c) 21 | } 22 | } 23 | func main() { 24 | } 25 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | TODO 2 | ---- 3 | 4 | Figure out CRLF nonsense 5 | Figure out URIs 6 | Mailing list extensions 7 | Be more precise about datetime format 8 | Figure out if ED25519 is OK to put in the spec of if I should be agnostic somehow (I know there is some protocol for this) 9 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | Nanomail Specification (v0.0.0 DRAFT) 2 | ============== 3 | 4 | 1 Overview 5 | ---------- 6 | 7 | Nanomail is a text-based internet communication protocol, inspired by email. 8 | 9 | A nanomail server runs on port 1999 and handles receiving messages. A nanomail 10 | client signs messages and sends them to the destination server. 11 | 12 | Nanomail combines many technologies that make up email infrastructure, like 13 | SMTP, POP and DMARC into a single, dramatically simpler specification. 14 | 15 | Nanomail is designed around personal, text-based communication, as well as for 16 | online public communication, such as discussion boards. It should be simple 17 | enough for a basic implementation to be easy, while thorough enough for public 18 | and private communication. 19 | 20 | All requests should be encased in TLS. 21 | 22 | Structure of a message 23 | --------------------- 24 | 25 | Nanomail messages look a lot like email messages. They have these 26 | headers, in the following order, followed by an empty line: 27 | 28 | ``` 29 | Signature: abcdef123 30 | From: sally@example.com 31 | To: bob@gmail.com 32 | Sent-At: 2022-03-01T01:23:45Z 33 | Thread-Id: def789 34 | Subject: My Email 35 | 36 | Message body 37 | ``` 38 | 39 | Header values MUST NOT contain newline characters. Header names MUST include a 40 | space after the colon. Header names are not case sensitive. Headers SHOULD NOT 41 | be duplicated, duplicate headers may be discarded. Header order after the 42 | signature is not relevant 43 | 44 | The body of a nanomail message MUST consist of UTF-8 formatted gemtext, as 45 | described in the [Gemini specification](https://gemini.circumlunar.space/docs/specification.gmi). 46 | 47 | Email addresses must be of the format: 48 | 49 | ``` 50 | [some-text]@[valid URI] 51 | ``` 52 | 53 | Some-text can't include an @ symbol. TBD how to define a URI 54 | 55 | Sent-at is a datetime in UTC that must follow this RFC-3339 format: 56 | YYYY-MM-DDTHH:MM:SSZ. 57 | 58 | Unlike in email, sent-at should be relatively close to received-at -- each attempt at delivery should update the sent-at field and re-sign the message. 59 | 60 | Thread-Id represents the thread that a message may be a part of. Threads 61 | consist of a linear, not tree-based structure. 62 | 63 | Signatures, or some truncated form of it, may be used as a 'message ID', e.g. 64 | for generating hyperlinks in a web forum. 65 | 66 | Threading 67 | ---------- 68 | 69 | Clients SHOULD NOT change subjects for messages that are in the same thread. 70 | 71 | Threads SHOULD be sorted by Sent-At. 72 | 73 | 74 | Signing the message 75 | ------------------ 76 | 77 | Authentication is handled outside of the nanomail system. You are responsible for A. saving a message and B. putting your public key somewhere public. 78 | 79 | This is sort of like DMARC, but uses the server instead of DNS records. 80 | 81 | We use an asymmetric key algorithm to verify the integrity and 82 | authenticity of a message. 83 | 84 | Signatures should use ED25519. 85 | 86 | The signature signs everything after the newline at the end of the signature. 87 | 88 | Nanomail servers SHOULD verify that the Sent-At header of the message is within 89 | some reasonable time period. 90 | 91 | Sending mail 92 | ------------ 93 | 94 | A nanomail message represents one-to-one communication between a single sender 95 | and a single recepient. Think like physical mail in this anology. 96 | 97 | The nanomail client is solely responsible for sending messages to a server, similar to SMTP. 98 | 99 | No multiple recipients here. Think like physical mail 100 | 101 | A client request consists of a command (in all caps) followed by CRLF and a message. 102 | 103 | ``` 104 | SEND 105 | Signature: abcdef123 106 | From: sally@example.com 107 | To: bob@whatever.example 108 | Subject: My Email 109 | 110 | Hello bob. Thanks for reading my email. 111 | ``` 112 | 113 | 114 | whatever.example then makes a request to the nanomail server at example.com (if it doesnt have the key already): 115 | 116 | ``` 117 | GETKEY sally 118 | ``` 119 | 120 | Which fetches the public keyy and validates the signature. OR maybe it returns 121 | a URL where you can get the key? which could be either gemini:// or https:// or 122 | something else? this way, you can update your key without talking to the 123 | server? 124 | 125 | This URL looks for the string nmail:somekey123 on the page. This is the cerca 126 | model of auth https://github.com/cblgh/cerca 127 | 128 | If key validation works, server will respond OK, else some error code 129 | 130 | status CODES (not complete): 131 | ``` 132 | 20 OK 133 | 40 INVALID REQUEST 134 | 41 USER DNE 135 | 51 SIGNATURE INVALID 136 | ``` 137 | 138 | etc 139 | 140 | Receiving mail 141 | ------------- 142 | 143 | Signature validation TBD 144 | 145 | Servers SHOULD validate that the Sent-At time is reasonably accurate, ie, 146 | consistent with a reasonable time that the message would take to be delivered. 147 | 148 | 149 | Pulling mail (client-server) 150 | --------------------------- 151 | 152 | Use private key authentication 153 | 154 | ``` 155 | FETCH sally [signature] 156 | ``` 157 | 158 | Returns messages if there are any. Each request returns a message, or DONE 159 | 160 | Servers SHOULD delete the message after it is fetched. 161 | 162 | Registering account (client-server) 163 | ------------------- 164 | 165 | REGISTER uname (URL) 166 | 167 | If URL is one of the allowlisted URL hosts and uname DNE, you are registered. 168 | 169 | The URL should be a place containing the public ed25519 key (e.g. a personal website) 170 | 171 | You should keep this URL updated with your public key so that you can reliably send messages 172 | 173 | Handling spam 174 | ------------- 175 | 176 | No one will use this, so no worries about spam. If they do, consider a server 177 | allowlist or blocklist (like how fediverse handles these things). Nanomail is a 178 | "human-scale" technology: if your server becomes large enough that you cannot 179 | adequately moderate it, you should cap signups. Encourage self-hosting and 180 | single-user or few-user instances. 181 | 182 | Server administrators should reserve postmaster@[host] to respond to any problems or abuse 183 | --------------------------------------------------------------------------------