Initial commit

This commit is contained in:
Neil Alexander
2021-07-07 18:15:07 +01:00
commit ceffe7612d
26 changed files with 2130 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.db
.DS_Store

66
README.md Normal file
View File

@@ -0,0 +1,66 @@
# Yggmail
It's email, but not as you know it.
## Introduction
Yggmail is a single-binary all-in-one mail transfer agent which sends and receives email natively over the Yggdrasil Network.
* Yggmail runs just about anywhere you like — your inbox is stored right on your own machine;
* Implements IMAP and SMTP protocols for sending and receiving mail, so you can use your favourite client (hopefully);
* Mails are exchanged between Yggmail users using built-in Yggdrasil connectivity;
* All mail exchange traffic between any two Yggmail nodes is always end-to-end encrypted without exception;
* Yggdrasil and Yggmail nodes on the same network are discovered automatically using multicast or you can configure a static Yggdrasil peer.
Email addresses are based on your public key, like `neilalexander@e3bf4665ae1ff714e0112040af8ddfc8e4b664a28e4afa40746e13952550f9ef`.
## Quickstart
Build Yggmail by installing a recent version of Go:
```
go install github.com/neilalexander/yggmail/cmd/yggmail
```
Create a mailbox, e.g. for user `alice`. A database will automatically be created in your working directory:
```
yggmail --createuser alice
```
Start Yggmail, using the database in your working directory:
```
yggmail -smtp=localhost:1025 -imap=localhost:1026
```
Connect your mail client to Yggmail. In the above example:
* SMTP is listening on `localhost` port 1025, password authentication, no SSL/TLS
* IMAP is listening on `localhost` port 1026, password authentication, no SSL/TLS
Then try sending a mail to another Yggmail user!
## Parameters
The following command line switches are supported by the `yggmail` binary:
* `-peer tls://...` or `-peer tcp://...` — connect to a specific Yggdrasil node;
* `-database /path/to/yggmail.db` — use a specific database file;
* `-smtp listenaddr:port` — listen for SMTP on a specific address/port
* `-imap listenaddr:port` — listen for IMAP on a specific address/port;
* `-createuser username` — create a new user in the database (doesn't matter if Yggmail is running or not, just make sure that Yggmail is pointing at the right database file or that you are in the right working directory).
## Notes
There are a few important notes:
* Yggmail needs to be running in order to receive inbound emails — it's therefore important to run Yggmail somewhere that will have good uptime;
* Yggmail tries to guarantee that senders are who they say they are. Your `From` address must be your Yggmail address (or at the very least, from your Yggmail domain);
* You can only email other Yggmail users, not regular email addresses on the public Internet;
* You may need to configure your client to allow "insecure" or "plaintext" authentication to IMAP/SMTP — this is because we don't support SSL/TLS on the IMAP/SMTP listeners yet.
## Bugs
There are probably all sorts of bugs, but the ones that we know of are:
* IMAP behaviour might not be entirely spec-compliant in all cases, so your mileage with mail clients might vary;
* SMTP queues up outbound mails in memory rather than in the database right now — if you restart Yggmail, any unsent mails will be lost.

191
cmd/yggmail/main.go Normal file
View File

@@ -0,0 +1,191 @@
package main
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"flag"
"fmt"
"log"
"os"
"strings"
"sync"
"github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"golang.org/x/term"
"github.com/neilalexander/yggmail/internal/config"
"github.com/neilalexander/yggmail/internal/imapserver"
"github.com/neilalexander/yggmail/internal/smtpsender"
"github.com/neilalexander/yggmail/internal/smtpserver"
"github.com/neilalexander/yggmail/internal/storage/sqlite3"
"github.com/neilalexander/yggmail/internal/transport"
)
var database = flag.String("database", "yggmail.db", "SQLite database file")
var smtpaddr = flag.String("smtp", "localhost:1025", "SMTP listen address")
var imapaddr = flag.String("imap", "localhost:1026", "IMAP listen address")
var peeraddr = flag.String("peer", "", "Yggdrasil static peer")
var createuser = flag.String("createuser", "", "Create a user")
func main() {
flag.Parse()
rawlog := log.New(os.Stdout, "", 0)
log := log.New(rawlog.Writer(), "[ \033[32mYggmail\033[0m ] ", 0)
storage, err := sqlite3.NewSQLite3StorageStorage(*database)
if err != nil {
panic(err)
}
log.Printf("Using database file %q\n", *database)
skStr, err := storage.ConfigGet("private_key")
if err != nil {
panic(err)
}
sk := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
if skStr == "" {
if _, sk, err = ed25519.GenerateKey(nil); err != nil {
panic(err)
}
if err := storage.ConfigSet("private_key", hex.EncodeToString(sk)); err != nil {
panic(err)
}
log.Printf("Generated new server identity")
} else {
skBytes, err := hex.DecodeString(skStr)
if err != nil {
panic(err)
}
copy(sk, skBytes)
}
pk := sk.Public().(ed25519.PublicKey)
log.Println("Mail domain:", hex.EncodeToString(pk))
switch {
case createuser != nil && *createuser != "":
fmt.Printf("New password: ")
password1, err := term.ReadPassword(0)
if err != nil {
panic(err)
}
fmt.Println()
fmt.Printf("Confirm password: ")
password2, err := term.ReadPassword(0)
if err != nil {
panic(err)
}
fmt.Println()
if !bytes.Equal(password1, password2) {
fmt.Println("The supplied passwords do not match")
os.Exit(1)
}
if err := storage.CreateUser(*createuser, strings.TrimSpace(string(password1))); err != nil {
fmt.Printf("Failed to create user %q\n", *createuser)
os.Exit(1)
}
if err := storage.MailboxCreate(*createuser, "INBOX"); err != nil {
panic(err)
}
fmt.Printf("Created user %q\n", *createuser)
fmt.Printf("Email address will be %s@%s\n", *createuser, hex.EncodeToString(pk))
os.Exit(0)
}
cfg := &config.Config{
PublicKey: pk,
PrivateKey: sk,
}
wg := &sync.WaitGroup{}
wg.Add(2)
transport, err := transport.NewYggdrasilTransport(rawlog, sk, pk, *peeraddr)
if err != nil {
panic(err)
}
queues := smtpsender.NewQueues(cfg, log, transport)
go func() {
defer wg.Done()
imapBackend := &imapserver.Backend{
Log: log,
Config: cfg,
Storage: storage,
}
imapServer := server.New(imapBackend)
imapServer.Addr = *imapaddr
imapServer.AllowInsecureAuth = true
imapServer.EnableAuth(sasl.Login, func(conn server.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
_, err := imapBackend.Login(nil, username, password)
return err
})
})
log.Println("Listening for IMAP on:", imapServer.Addr)
if err := imapServer.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
go func() {
defer wg.Done()
localBackend := &smtpserver.Backend{
Log: log,
Mode: smtpserver.BackendModeInternal,
Config: cfg,
Storage: storage,
Queues: queues,
}
localServer := smtp.NewServer(localBackend)
localServer.Addr = *smtpaddr
localServer.Domain = hex.EncodeToString(pk)
localServer.MaxMessageBytes = 1024 * 1024
localServer.MaxRecipients = 50
localServer.AllowInsecureAuth = true
localServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
_, err := localBackend.Login(nil, username, password)
return err
})
})
log.Println("Listening for SMTP on:", localServer.Addr)
if err := localServer.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
go func() {
defer wg.Done()
overlayBackend := &smtpserver.Backend{
Log: log,
Mode: smtpserver.BackendModeExternal,
Config: cfg,
Storage: storage,
Queues: queues,
}
overlayServer := smtp.NewServer(overlayBackend)
overlayServer.Domain = hex.EncodeToString(pk)
overlayServer.MaxMessageBytes = 1024 * 1024
overlayServer.MaxRecipients = 50
overlayServer.AuthDisabled = true
if err := overlayServer.Serve(transport.Listener()); err != nil {
log.Fatal(err)
}
}()
wg.Wait()
}

101
cmd/yggproxy/main.go Normal file
View File

@@ -0,0 +1,101 @@
package main
import (
"crypto/ed25519"
"encoding/hex"
"flag"
"log"
"net"
"os"
"github.com/neilalexander/yggmail/internal/transport"
)
var dst = flag.String("dst", "", "Destination public key to proxy to")
var peeraddr = flag.String("peer", "", "Yggdrasil static peer")
func main() {
flag.Parse()
/*
pk, sk, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
*/
sk := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
if sks, err := hex.DecodeString("f50a6a4688ca602307dcf282304583b1746093a558e934d3e9a817bb1e7be77b7c824efcd702e80a3a6912e15ebc4e13454022947ce8ee46ddb871e8b9a9147f"); err != nil {
panic(err)
} else {
copy(sk, sks)
}
pk := sk.Public().(ed25519.PublicKey)
log.Println("Private key:", hex.EncodeToString(sk))
log := log.New(os.Stdout, "", 0)
transport, err := transport.NewYggdrasilTransport(log, sk, pk, *peeraddr)
if err != nil {
panic(err)
}
listener, err := net.Listen("tcp", "localhost:1026")
if err != nil {
panic(err)
}
log.Println("Proxying", listener.Addr(), "to", *dst)
for {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
log.Println("Accepted connection from", conn.RemoteAddr())
upstream, err := transport.Dial(*dst)
if err != nil {
log.Println("Failed to dial upstream:", err)
conn.Close()
continue
}
go func(conn, upstream net.Conn) {
defer conn.Close()
defer upstream.Close()
var b [1024]byte
for {
n, err := conn.Read(b[:])
if err != nil {
log.Println("conn.Read:", err)
return
}
_, err = upstream.Write(b[:n])
if err != nil {
log.Println("upstream.Write:", err)
return
}
}
}(conn, upstream)
go func(conn, upstream net.Conn) {
defer conn.Close()
defer upstream.Close()
var b [1024]byte
for {
n, err := upstream.Read(b[:])
if err != nil {
log.Println("upstream.Read:", err)
return
}
_, err = conn.Write(b[:n])
if err != nil {
log.Println("conn.Write:", err)
return
}
}
}(conn, upstream)
}
}

20
go.mod Normal file
View File

@@ -0,0 +1,20 @@
module github.com/neilalexander/yggmail
go 1.16
require (
github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031
github.com/emersion/go-imap v1.1.0
github.com/emersion/go-imap-idle v0.0.0-20201224103203-6f42b9020098
github.com/emersion/go-message v0.15.0
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.15.0
github.com/gologme/log v1.2.0
github.com/mattn/go-sqlite3 v1.14.7
github.com/neilalexander/utp v0.1.1-0.20210705212447-691f29ad692b
github.com/yggdrasil-network/yggdrasil-go v0.4.1-0.20210707004512-3704ebf4cbea
go.uber.org/atomic v1.7.0
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
)

152
go.sum Normal file
View File

@@ -0,0 +1,152 @@
github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031 h1:DZVDfYhVdu+0wAiRHoY1olyNkKxIot9UjBnbQFzuUlM=
github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031/go.mod h1:RP72rucOFm5udrnEzTmIWLRVGQiV/fSUAQXJ0RST/nk=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 h1:WndgpSW13S32VLQ3ugUxx2EnnWmgba1kCqPkd4Gk1yQ=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979/go.mod h1:6Lkn+/zJilRMsKmbmG1RPoamiArC6HS73xbwRyp3UyI=
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.1.1 h1:sHQCyj7HtiSfaZAzL2rJrQdyS7odLqlwO6nhk/tG/j8=
github.com/anacrolix/envpprof v1.1.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
github.com/anacrolix/log v0.3.0 h1:Btxh7GkT4JYWvWJ1uKOwgobf+7q/1eFQaDdCUXCtssw=
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
github.com/anacrolix/missinggo v1.2.1 h1:0IE3TqX5y5D0IxeMwTyIgqdDew4QrzcXaaEnJQyjHvw=
github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y=
github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw=
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
github.com/anacrolix/sync v0.2.0 h1:oRe22/ZB+v7v/5Mbc4d2zE0AXEZy0trKyKLjqYOt6tY=
github.com/anacrolix/sync v0.2.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-imap v1.0.6/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/emersion/go-imap v1.1.0 h1:hAW8Dbi/AwiVO5Wi40FTVuCzVrTmwtEK6De9GSoOy+Y=
github.com/emersion/go-imap v1.1.0/go.mod h1:0hCeak4mA2z9hICM20jeqN6fyV0Oad0lZTyeeAyUS6o=
github.com/emersion/go-imap-idle v0.0.0-20201224103203-6f42b9020098 h1:J+qvrz94n18fVThwhUWwrBwRbcNqi+VgcUJlaph430A=
github.com/emersion/go-imap-idle v0.0.0-20201224103203-6f42b9020098/go.mod h1:N/6S3dRTVt8xT867m+476C16+v/Fq4WZYvh2Chg0nmg=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.14.1/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kardianos/minwinsvc v1.0.0/go.mod h1:Bgd0oc+D0Qo3bBytmNtyRKVlp85dAloLKhfxanPFFRc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/neilalexander/utp v0.1.1-0.20210705212447-691f29ad692b h1:XNm+Ks3bVziRJxcMaIbzumWEw7l52z9Rek6cMHgln1g=
github.com/neilalexander/utp v0.1.1-0.20210705212447-691f29ad692b/go.mod h1:ylsx0342RjGHjOoVKhR/wz/7Lhiusonihfj4QLxEMcU=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/yggdrasil-network/yggdrasil-go v0.4.1-0.20210707004512-3704ebf4cbea h1:3J6MCIkRIlkBHqSDU+ryRGDMamLgs9S7eyB4DNSdgX0=
github.com/yggdrasil-network/yggdrasil-go v0.4.1-0.20210707004512-3704ebf4cbea/go.mod h1:/iMJjOrXRsjlFgqhWOPhecOKi7xHmHiY4/En3A42Fog=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7-0.20210503195748-5c7c50ebbd4f h1:yQJrRE0hDxDFmZLlRaw+3vusO4fwNHgHIjUOMO7bHYI=
golang.org/x/text v0.3.7-0.20210503195748-5c7c50ebbd4f/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.0-20210510202332-9844c74f67ec/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard/windows v0.3.14/go.mod h1:3P4IEAsb+BjlKZmpUXgy74c0iX9AVwwr3WcVJ8nPgME=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

10
internal/config/config.go Normal file
View File

@@ -0,0 +1,10 @@
package config
import (
"crypto/ed25519"
)
type Config struct {
PublicKey ed25519.PublicKey
PrivateKey ed25519.PrivateKey
}

View File

@@ -0,0 +1,33 @@
package imapserver
import (
"fmt"
"log"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
"github.com/neilalexander/yggmail/internal/config"
"github.com/neilalexander/yggmail/internal/storage"
)
type Backend struct {
Config *config.Config
Log *log.Logger
Storage storage.Storage
}
func (b *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.User, error) {
if authed, err := b.Storage.TryAuthenticate(username, password); err != nil {
b.Log.Printf("Failed to authenticate IMAP user %q due to error: %s", username, err)
return nil, fmt.Errorf("failed to authenticate: %w", err)
} else if !authed {
b.Log.Printf("Failed to authenticate IMAP user %q\n", username)
return nil, backend.ErrInvalidCredentials
}
defer b.Log.Printf("Authenticated IMAP user %q\n", username)
user := &User{
backend: b,
username: username,
}
return user, nil
}

View File

@@ -0,0 +1,20 @@
package imapserver
import (
idle "github.com/emersion/go-imap-idle"
"github.com/emersion/go-imap/server"
)
type IMAPServer struct {
server *server.Server
backend *Backend
}
func NewIMAPServer(backend *Backend) (*IMAPServer, error) {
s := &IMAPServer{
server: server.New(backend),
backend: backend,
}
s.server.Enable(idle.NewExtension())
return s, nil
}

View File

@@ -0,0 +1,293 @@
package imapserver
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend/backendutil"
"github.com/emersion/go-message/textproto"
)
type Mailbox struct {
backend *Backend
name string
user *User
}
func (mbox *Mailbox) getIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]int32, error) {
var ids []int32
for _, set := range seqSet.Set {
for i := set.Start; i <= set.Stop; i++ {
if !uid {
pid, err := mbox.backend.Storage.MailIDForSeq(mbox.user.username, mbox.name, int(i))
if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailIDForSeq: %w", err)
}
ids = append(ids, int32(pid))
} else {
ids = append(ids, int32(i))
}
}
}
return ids, nil
}
func (mbox *Mailbox) Name() string {
return mbox.name
}
func (mbox *Mailbox) Info() (*imap.MailboxInfo, error) {
info := &imap.MailboxInfo{
Attributes: []string{},
Delimiter: "/",
Name: mbox.name,
}
return info, nil
}
func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
status := imap.NewMailboxStatus(mbox.name, items)
status.PermanentFlags = []string{
"\\Seen", "\\Answered", "\\Flagged", "\\Deleted",
}
status.Flags = status.PermanentFlags
for _, name := range items {
switch name {
case imap.StatusMessages:
count, err := mbox.backend.Storage.MailCount(mbox.user.username, mbox.name)
if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailCount: %w", err)
}
status.Messages = uint32(count)
case imap.StatusUidNext:
id, err := mbox.backend.Storage.MailNextID(mbox.user.username, mbox.name)
if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailNextID: %w", err)
}
status.UidNext = uint32(id)
case imap.StatusUidValidity:
status.UidValidity = 1
case imap.StatusRecent:
status.Recent = 0 // TODO
case imap.StatusUnseen:
unseen, err := mbox.backend.Storage.MailUnseen(mbox.user.username, mbox.name)
if err != nil {
return nil, fmt.Errorf("mbox.backend.Storage.MailUnseen: %w", err)
}
status.Unseen = uint32(unseen)
}
}
return status, nil
}
func (mbox *Mailbox) SetSubscribed(subscribed bool) error {
return mbox.backend.Storage.MailboxSubscribe(mbox.user.username, mbox.name, subscribed)
}
func (mbox *Mailbox) Check() error {
return nil
}
func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error {
defer close(ch)
ids, err := mbox.getIDsFromSeqSet(uid, seqSet)
if err != nil {
fmt.Println("Failed to get IDs from sequences:", err)
return fmt.Errorf("mbox.getIDsFromSeqSet: %w", err)
}
for _, id := range ids {
mseq, mid, body, seen, answered, flagged, deleted, datetime, err := mbox.backend.Storage.MailSelect(mbox.user.username, mbox.name, int(id))
if err != nil {
continue
}
fetched := imap.NewMessage(uint32(id)+1, items)
fetched.SeqNum = uint32(mseq)
fetched.Uid = uint32(mid)
get := func() (io.Reader, textproto.Header, error) {
bodyreader := bufio.NewReader(bytes.NewReader(body))
hdr, err := textproto.ReadHeader(bodyreader)
if err != nil {
return nil, textproto.Header{}, fmt.Errorf("textproto.ReadHeader: %w", err)
}
return bodyreader, hdr, err
}
for _, item := range items {
switch item {
case imap.FetchEnvelope:
_, hdr, err := get()
if err != nil {
continue
}
if fetched.Envelope, err = backendutil.FetchEnvelope(hdr); err != nil {
continue
}
case imap.FetchBody, imap.FetchBodyStructure:
bodyreader, hdr, err := get()
if err != nil {
continue
}
if fetched.BodyStructure, err = backendutil.FetchBodyStructure(hdr, bodyreader, item == imap.FetchBodyStructure); err != nil {
continue
}
case imap.FetchFlags:
fetched.Flags = []string{}
if seen {
fetched.Flags = append(fetched.Flags, "\\Seen")
}
if answered {
fetched.Flags = append(fetched.Flags, "\\Answered")
}
if flagged {
fetched.Flags = append(fetched.Flags, "\\Flagged")
}
if deleted {
fetched.Flags = append(fetched.Flags, "\\Deleted")
}
case imap.FetchInternalDate:
fetched.InternalDate = datetime
case imap.FetchRFC822Size:
fetched.Size = uint32(len(body))
case imap.FetchUid:
fetched.Uid = uint32(id)
default:
section, err := imap.ParseBodySectionName(item)
if err != nil {
continue
}
bodyreader, hdr, err := get()
if err != nil {
continue
}
l, err := backendutil.FetchBodySection(hdr, bodyreader, section)
if err != nil {
continue
}
fetched.Body[section] = l
}
}
ch <- fetched
}
return nil
}
func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) {
return mbox.backend.Storage.MailSearch(mbox.user.username, mbox.name)
}
func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
b, err := ioutil.ReadAll(body)
if err != nil {
return fmt.Errorf("b.ReadFrom: %w", err)
}
id, err := mbox.backend.Storage.MailCreate(mbox.user.username, mbox.name, b)
if err != nil {
return fmt.Errorf("mbox.backend.Storage.MailCreate: %w", err)
}
for _, flag := range flags {
var seen, answered, flagged, deleted bool
switch flag {
case "\\Seen":
seen = true
case "\\Answered":
answered = true
case "\\Flagged":
flagged = true
case "\\Deleted":
deleted = true
}
if err := mbox.backend.Storage.MailUpdateFlags(
mbox.user.username, mbox.name, id, seen, answered, flagged, deleted,
); err != nil {
return err
}
}
return nil
}
func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, op imap.FlagsOp, flags []string) error {
ids, err := mbox.getIDsFromSeqSet(uid, seqSet)
if err != nil {
return fmt.Errorf("mbox.getIDsFromSeqSet: %w", err)
}
for _, id := range ids {
var seen, answered, flagged, deleted bool
var mid int
if op != imap.SetFlags {
var err error
_, mid, _, seen, answered, flagged, deleted, _, err = mbox.backend.Storage.MailSelect(mbox.user.username, mbox.name, int(id))
if err != nil {
return fmt.Errorf("mbox.backend.Storage.MailSelect: %w", err)
}
}
for _, flag := range flags {
switch flag {
case "\\Seen":
seen = op != imap.RemoveFlags
case "\\Answered":
answered = op != imap.RemoveFlags
case "\\Flagged":
flagged = op != imap.RemoveFlags
case "\\Deleted":
deleted = op != imap.RemoveFlags
}
}
if err := mbox.backend.Storage.MailUpdateFlags(
mbox.user.username, mbox.name, int(mid), seen, answered, flagged, deleted,
); err != nil {
return err
}
}
return nil
}
func (mbox *Mailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, destName string) error {
ids, err := mbox.getIDsFromSeqSet(uid, seqSet)
if err != nil {
return fmt.Errorf("mbox.getIDsFromSeqSet: %w", err)
}
for _, id := range ids {
_, _, body, seen, answered, flagged, deleted, _, err := mbox.backend.Storage.MailSelect(mbox.user.username, mbox.name, int(id))
if err != nil {
return fmt.Errorf("mbox.backend.Storage.MailSelect: %w", err)
}
pid, err := mbox.backend.Storage.MailCreate(mbox.user.username, destName, body)
if err != nil {
return fmt.Errorf("mbox.backend.Storage.MailCreate: %w", err)
}
if err = mbox.backend.Storage.MailUpdateFlags(mbox.user.username, mbox.name, pid, seen, answered, flagged, deleted); err != nil {
return fmt.Errorf("mbox.backend.Storage.MailUpdateFlags: %w", err)
}
}
return nil
}
func (mbox *Mailbox) Expunge() error {
return mbox.backend.Storage.MailExpunge(mbox.user.username, mbox.name)
}

View File

@@ -0,0 +1,75 @@
package imapserver
import (
"errors"
"fmt"
"github.com/emersion/go-imap/backend"
)
type User struct {
backend *Backend
username string
}
func (u *User) Username() string {
return u.username
}
func (u *User) ListMailboxes(subscribed bool) (mailboxes []backend.Mailbox, err error) {
names, err := u.backend.Storage.MailboxList(u.username, subscribed)
if err != nil {
return nil, err
}
for _, mailbox := range names {
mailboxes = append(mailboxes, &Mailbox{
backend: u.backend,
user: u,
name: mailbox,
})
}
return
}
func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) {
if name == "" {
return &Mailbox{
backend: u.backend,
user: u,
name: "",
}, nil
}
ok, _ := u.backend.Storage.MailboxSelect(u.username, name)
if !ok {
return nil, fmt.Errorf("mailbox %q not found", name)
}
return &Mailbox{
backend: u.backend,
user: u,
name: name,
}, nil
}
func (u *User) CreateMailbox(name string) error {
return u.backend.Storage.MailboxCreate(u.username, name)
}
func (u *User) DeleteMailbox(name string) error {
if name == "INBOX" {
return errors.New("Cannot delete INBOX")
}
return u.backend.Storage.MailboxDelete(u.username, name)
}
func (u *User) RenameMailbox(existingName, newName string) error {
if existingName == "INBOX" {
return errors.New("Cannot rename INBOX")
}
return u.backend.Storage.MailboxRename(u.username, existingName, newName)
}
func (u *User) Logout() error {
return nil
}

View File

@@ -0,0 +1,58 @@
package smtpsender
import (
"sync"
)
type fifoQueue struct {
frames []interface{}
count int
mutex sync.Mutex
notifs chan struct{}
}
func newFIFOQueue() *fifoQueue {
q := &fifoQueue{
notifs: make(chan struct{}),
}
return q
}
func (q *fifoQueue) push(frame interface{}) bool {
q.mutex.Lock()
defer q.mutex.Unlock()
q.frames = append(q.frames, frame)
q.count++
select {
case q.notifs <- struct{}{}:
default:
}
return true
}
func (q *fifoQueue) pop() (interface{}, bool) {
q.mutex.Lock()
defer q.mutex.Unlock()
if q.count == 0 {
return nil, false
}
frame := q.frames[0]
q.frames[0] = nil
q.frames = q.frames[1:]
q.count--
if q.count == 0 {
q.frames = nil
}
return frame, true
}
func (q *fifoQueue) wait() <-chan struct{} {
q.mutex.Lock()
defer q.mutex.Unlock()
if q.count > 0 {
ch := make(chan struct{})
close(ch)
return ch
}
return q.notifs
}

View File

@@ -0,0 +1,136 @@
package smtpsender
import (
"encoding/hex"
"fmt"
"log"
"math"
"sync"
"time"
"github.com/emersion/go-smtp"
"github.com/neilalexander/yggmail/internal/config"
"github.com/neilalexander/yggmail/internal/transport"
"go.uber.org/atomic"
)
type Queues struct {
Config *config.Config
Log *log.Logger
Transport transport.Transport
queues sync.Map // servername -> *Queue
}
func NewQueues(config *config.Config, log *log.Logger, transport transport.Transport) *Queues {
return &Queues{
Config: config,
Log: log,
Transport: transport,
}
}
func (qs *Queues) QueueFor(server string) (*Queue, error) {
v, _ := qs.queues.LoadOrStore(server, &Queue{
queues: qs,
destination: server,
fifo: newFIFOQueue(),
})
q, ok := v.(*Queue)
if !ok {
return nil, fmt.Errorf("type assertion error")
}
if q.running.CAS(false, true) {
go q.run()
}
return q, nil
}
type Queue struct {
queues *Queues
destination string
running atomic.Bool
backoff atomic.Int64
fifo *fifoQueue
}
func (q *Queue) Queue(mail *QueuedMail) error {
q.fifo.push(mail)
if q.running.CAS(false, true) {
go q.run()
}
return nil
}
func (q *Queue) run() {
defer q.running.Store(false)
for {
select {
case <-q.fifo.wait():
case <-time.After(time.Second * 10):
return
}
item, ok := q.fifo.pop()
if !ok {
continue
}
mail, ok := item.(*QueuedMail)
if !ok {
continue
}
q.queues.Log.Println("Processing mail from", mail.From, "to", mail.Destination)
if err := func() error {
conn, err := q.queues.Transport.Dial(q.destination)
if err != nil {
return fmt.Errorf("q.queues.Transport.Dial: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, q.destination)
if err != nil {
return fmt.Errorf("smtp.NewClient: %w", err)
}
defer client.Close()
if err := client.Hello(hex.EncodeToString(q.queues.Config.PublicKey)); err != nil {
return fmt.Errorf("client.Hello: %w", err)
}
q.backoff.Store(0)
if err := client.Mail(mail.From, nil); err != nil {
return fmt.Errorf("client.Mail: %w", err)
}
if err := client.Rcpt(mail.Rcpt); err != nil {
return fmt.Errorf("client.Rcpt: %w", err)
}
writer, err := client.Data()
if err != nil {
return fmt.Errorf("client.Data: %w", err)
}
defer writer.Close()
if _, err := writer.Write(mail.Content); err != nil {
return fmt.Errorf("writer.Write: %w", err)
}
return nil
}(); err != nil {
retry := time.Second * time.Duration(math.Exp2(float64(q.backoff.Inc())))
q.queues.Log.Println("Queue error:", err, "- will retry in", retry)
time.Sleep(retry)
} else {
q.queues.Log.Println("Sent mail from", mail.From, "to", mail.Destination)
}
}
}
type QueuedMail struct {
From string // mail address
Rcpt string // mail addresses
Destination string // server name
Content []byte
}

View File

@@ -0,0 +1,79 @@
package smtpserver
import (
"encoding/hex"
"fmt"
"log"
"github.com/emersion/go-smtp"
"github.com/neilalexander/yggmail/internal/config"
"github.com/neilalexander/yggmail/internal/smtpsender"
"github.com/neilalexander/yggmail/internal/storage"
)
type BackendMode int
const (
BackendModeInternal BackendMode = iota
BackendModeExternal
)
type Backend struct {
Mode BackendMode
Log *log.Logger
Config *config.Config
Queues *smtpsender.Queues
Storage storage.Storage
}
func (b *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
switch b.Mode {
case BackendModeInternal:
// The connection came from our local listener
if authed, err := b.Storage.TryAuthenticate(username, password); err != nil {
b.Log.Printf("Failed to authenticate SMTP user %q due to error: %s", username, err)
return nil, fmt.Errorf("failed to authenticate: %w", err)
} else if !authed {
b.Log.Printf("Failed to authenticate SMTP user %q\n", username)
return nil, smtp.ErrAuthRequired
}
defer b.Log.Printf("Authenticated SMTP user %q\n", username)
return &SessionLocal{
backend: b,
state: state,
}, nil
case BackendModeExternal:
return nil, fmt.Errorf("Not expecting authenticated connection on external backend")
}
return nil, fmt.Errorf("Authenticated login failed")
}
func (b *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
switch b.Mode {
case BackendModeInternal:
return nil, fmt.Errorf("Not expecting anonymous connection on internal backend")
case BackendModeExternal:
// The connection came from our overlay listener, so we should check
// that they are who they claim to be
if state.Hostname != state.RemoteAddr.String() {
return nil, fmt.Errorf("You are not who you claim to be")
}
pks, err := hex.DecodeString(state.RemoteAddr.String())
if err != nil {
return nil, fmt.Errorf("hex.DecodeString: %w", err)
}
b.Log.Println("Incoming SMTP session from", state.RemoteAddr.String())
return &SessionRemote{
backend: b,
state: state,
public: pks[:],
}, nil
}
return nil, fmt.Errorf("Anonymous login failed")
}

View File

@@ -0,0 +1,14 @@
package smtpserver
import (
"fmt"
"strings"
)
func parseAddress(email string) (string, string, error) {
at := strings.LastIndex(email, "@")
if at == 0 {
return "", "", fmt.Errorf("invalid email address")
}
return email[:at], email[at+1:], nil
}

View File

@@ -0,0 +1,110 @@
package smtpserver
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"time"
"github.com/emersion/go-message"
"github.com/emersion/go-smtp"
"github.com/neilalexander/yggmail/internal/smtpsender"
)
type SessionLocal struct {
backend *Backend
state *smtp.ConnectionState
from string
rcpt []string
}
func (s *SessionLocal) Mail(from string, opts smtp.MailOptions) error {
_, host, err := parseAddress(from)
if err != nil {
return fmt.Errorf("parseAddress: %w", err)
}
if host != hex.EncodeToString(s.backend.Config.PublicKey) {
return fmt.Errorf("not allowed to send outgoing mail as %s", from)
}
s.from = from
return nil
}
func (s *SessionLocal) Rcpt(to string) error {
s.rcpt = append(s.rcpt, to)
return nil
}
func (s *SessionLocal) Data(r io.Reader) error {
m, err := message.Read(r)
if err != nil {
return fmt.Errorf("message.Read: %w", err)
}
m.Header.Add(
"Received", fmt.Sprintf("from %s by Yggmail %s; %s",
s.state.RemoteAddr.String(),
hex.EncodeToString(s.backend.Config.PublicKey),
time.Now().String(),
),
)
servers := make(map[string]struct{})
for _, rcpt := range s.rcpt {
localpart, host, err := parseAddress(rcpt)
if err != nil {
return fmt.Errorf("parseAddress: %w", err)
}
if _, ok := servers[host]; ok {
continue
}
servers[host] = struct{}{}
if host == hex.EncodeToString(s.backend.Config.PublicKey) {
var b bytes.Buffer
if err := m.WriteTo(&b); err != nil {
return fmt.Errorf("m.WriteTo: %w", err)
}
if _, err := s.backend.Storage.MailCreate(localpart, "INBOX", b.Bytes()); err != nil {
return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err)
}
continue
}
queue, err := s.backend.Queues.QueueFor(host)
if err != nil {
return fmt.Errorf("s.backend.Queues.QueueFor: %w", err)
}
mail := &smtpsender.QueuedMail{
From: s.from,
Rcpt: rcpt,
Destination: host,
}
var b bytes.Buffer
if err := m.WriteTo(&b); err != nil {
return fmt.Errorf("m.WriteTo: %w", err)
}
mail.Content = b.Bytes()
if err := queue.Queue(mail); err != nil {
return fmt.Errorf("queue.Queue: %w", err)
}
s.backend.Log.Println("Queued mail for", mail.Destination)
}
return nil
}
func (s *SessionLocal) Reset() {}
func (s *SessionLocal) Logout() error {
return nil
}

View File

@@ -0,0 +1,82 @@
package smtpserver
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"fmt"
"io"
"time"
"github.com/emersion/go-message"
"github.com/emersion/go-smtp"
)
type SessionRemote struct {
backend *Backend
state *smtp.ConnectionState
public ed25519.PublicKey
from string
localparts []string
}
func (s *SessionRemote) Mail(from string, opts smtp.MailOptions) error {
_, host, err := parseAddress(from)
if err != nil {
return fmt.Errorf("mail.ParseAddress: %w", err)
}
if local := s.state.RemoteAddr.String(); local != host {
return fmt.Errorf("not allowed to send incoming mail as %s", from)
}
s.from = from
return nil
}
func (s *SessionRemote) Rcpt(to string) error {
user, host, err := parseAddress(to)
if err != nil {
return fmt.Errorf("mail.ParseAddress: %w", err)
}
if local := hex.EncodeToString(s.backend.Config.PublicKey); host != local {
return fmt.Errorf("not allowed to send mail to %q", host)
}
s.localparts = append(s.localparts, user)
return nil
}
func (s *SessionRemote) Data(r io.Reader) error {
m, err := message.Read(r)
if err != nil {
return fmt.Errorf("message.Read: %w", err)
}
m.Header.Add(
"Received", fmt.Sprintf("from Yggmail %s; %s",
hex.EncodeToString(s.public),
time.Now().String(),
),
)
var b bytes.Buffer
if err := m.WriteTo(&b); err != nil {
return fmt.Errorf("m.WriteTo: %w", err)
}
for _, localpart := range s.localparts {
if _, err := s.backend.Storage.MailCreate(localpart, "INBOX", b.Bytes()); err != nil {
return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err)
}
s.backend.Log.Printf("Stored new mail for local user %q from %s", localpart, s.from)
}
return nil
}
func (s *SessionRemote) Reset() {}
func (s *SessionRemote) Logout() error {
return nil
}

View File

@@ -0,0 +1,18 @@
package smtpserver
import (
"github.com/emersion/go-smtp"
)
type SMTPServer struct {
server *smtp.Server
backend smtp.Backend
}
func NewSMTPServer(backend smtp.Backend) *SMTPServer {
s := &SMTPServer{
server: smtp.NewServer(backend),
backend: backend,
}
return s
}

View File

@@ -0,0 +1,40 @@
package sqlite3
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
type SQLite3Storage struct {
*TableConfig
*TableUsers
*TableMailboxes
*TableMails
}
func NewSQLite3StorageStorage(filename string) (*SQLite3Storage, error) {
db, err := sql.Open("sqlite3", "file:"+filename+"?_foreign_keys=on")
if err != nil {
return nil, fmt.Errorf("sql.Open: %w", err)
}
s := &SQLite3Storage{}
s.TableConfig, err = NewTableConfig(db)
if err != nil {
return nil, fmt.Errorf("NewTableConfig: %w", err)
}
s.TableUsers, err = NewTableUsers(db)
if err != nil {
return nil, fmt.Errorf("NewTableUsers: %w", err)
}
s.TableMailboxes, err = NewTableMailboxes(db)
if err != nil {
return nil, fmt.Errorf("NewTableMailboxes: %w", err)
}
s.TableMails, err = NewTableMails(db)
if err != nil {
return nil, fmt.Errorf("NewTableMails: %w", err)
}
return s, nil
}

View File

@@ -0,0 +1,61 @@
package sqlite3
import (
"database/sql"
"fmt"
)
type TableConfig struct {
db *sql.DB
get *sql.Stmt
set *sql.Stmt
}
const configSchema = `
CREATE TABLE IF NOT EXISTS config (
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY(key)
);
`
const configGet = `
SELECT value FROM config WHERE key = $1
`
const configSet = `
INSERT OR REPLACE INTO config (key, value) VALUES($1, $2)
`
func NewTableConfig(db *sql.DB) (*TableConfig, error) {
t := &TableConfig{
db: db,
}
_, err := db.Exec(configSchema)
if err != nil {
return nil, fmt.Errorf("db.Exec: %w", err)
}
t.get, err = db.Prepare(configGet)
if err != nil {
return nil, fmt.Errorf("db.Prepare(get): %w", err)
}
t.set, err = db.Prepare(configSet)
if err != nil {
return nil, fmt.Errorf("db.Prepare(set): %w", err)
}
return t, nil
}
func (t *TableConfig) ConfigGet(key string) (string, error) {
var value string
err := t.get.QueryRow(key).Scan(&value)
if err == sql.ErrNoRows {
return "", nil
}
return value, err
}
func (t *TableConfig) ConfigSet(key, value string) error {
_, err := t.set.Exec(key, value)
return err
}

View File

@@ -0,0 +1,152 @@
package sqlite3
import (
"database/sql"
"fmt"
)
type TableMailboxes struct {
db *sql.DB
selectMailboxes *sql.Stmt
listMailboxes *sql.Stmt
listMailboxesSubscribed *sql.Stmt
createMailbox *sql.Stmt
renameMailbox *sql.Stmt
deleteMailbox *sql.Stmt
subscribeMailbox *sql.Stmt
}
const mailboxesSchema = `
CREATE TABLE IF NOT EXISTS mailboxes (
username TEXT NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
mailbox TEXT NOT NULL DEFAULT('INBOX'),
subscribed BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY(username, mailbox)
);
`
const mailboxesList = `
SELECT mailbox FROM mailboxes WHERE username = $1
`
const mailboxesListSubscribed = `
SELECT mailbox FROM mailboxes WHERE username = $1 AND subscribed = 1
`
const mailboxesSelect = `
SELECT mailbox FROM mailboxes WHERE username = $1 AND mailbox = $2
`
const mailboxesCreate = `
INSERT INTO mailboxes (username, mailbox) VALUES($1, $2)
`
const mailboxesRename = `
UPDATE mailboxes SET mailbox = $1 WHERE username = $2 AND mailbox = $3
`
const mailboxesDelete = `
DELETE FROM mailboxes WHERE username = $1 AND mailbox = $2
`
const mailboxesSubscribe = `
UPDATE mailboxes SET subscribed = $1 WHERE username = $2 AND mailbox = $3
`
func NewTableMailboxes(db *sql.DB) (*TableMailboxes, error) {
t := &TableMailboxes{
db: db,
}
_, err := db.Exec(mailboxesSchema)
if err != nil {
return nil, fmt.Errorf("db.Exec: %w", err)
}
t.listMailboxes, err = db.Prepare(mailboxesList)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesCreate): %w", err)
}
t.listMailboxesSubscribed, err = db.Prepare(mailboxesListSubscribed)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesCreate): %w", err)
}
t.selectMailboxes, err = db.Prepare(mailboxesSelect)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesSelect): %w", err)
}
t.createMailbox, err = db.Prepare(mailboxesCreate)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesCreate): %w", err)
}
t.deleteMailbox, err = db.Prepare(mailboxesDelete)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesDelete): %w", err)
}
t.renameMailbox, err = db.Prepare(mailboxesRename)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesRename): %w", err)
}
t.subscribeMailbox, err = db.Prepare(mailboxesSubscribe)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesSubscribe): %w", err)
}
return t, nil
}
func (t *TableMailboxes) MailboxList(user string, onlySubscribed bool) ([]string, error) {
stmt := t.listMailboxes
if onlySubscribed {
stmt = t.listMailboxesSubscribed
}
rows, err := stmt.Query(user)
if err != nil {
return nil, fmt.Errorf("t.listMailboxes.Query: %w", err)
}
defer rows.Close()
var mailboxes []string
for rows.Next() {
var mailbox string
if err := rows.Scan(&mailbox); err != nil {
return nil, fmt.Errorf("rows.Scan: %w", err)
}
mailboxes = append(mailboxes, mailbox)
}
return mailboxes, nil
}
func (t *TableMailboxes) MailboxSelect(user, mailbox string) (bool, error) {
row := t.selectMailboxes.QueryRow(user, mailbox)
if err := row.Err(); err != nil && err != sql.ErrNoRows {
return false, fmt.Errorf("row.Err: %w", err)
} else if err == sql.ErrNoRows {
return false, nil
}
var got string
if err := row.Scan(&got); err != nil {
return false, fmt.Errorf("row.Scan: %w", err)
}
return mailbox == got, nil
}
func (t *TableMailboxes) MailboxCreate(user, name string) error {
_, err := t.createMailbox.Exec(user, name)
return err
}
func (t *TableMailboxes) MailboxRename(user, old, new string) error {
_, err := t.renameMailbox.Exec(new, user, old)
return err
}
func (t *TableMailboxes) MailboxDelete(user, name string) error {
_, err := t.deleteMailbox.Exec(user, name)
return err
}
func (t *TableMailboxes) MailboxSubscribe(user, name string, subscribed bool) error {
sn := 1
if !subscribed {
sn = 0
}
_, err := t.subscribeMailbox.Exec(sn, user, name)
return err
}

View File

@@ -0,0 +1,227 @@
package sqlite3
import (
"database/sql"
"fmt"
"time"
)
type TableMails struct {
db *sql.DB
selectMails *sql.Stmt
selectMail *sql.Stmt
selectMailNextID *sql.Stmt
selectIDForSeq *sql.Stmt
searchMail *sql.Stmt
createMail *sql.Stmt
countMails *sql.Stmt
countUnseenMails *sql.Stmt
updateMailFlags *sql.Stmt
deleteMail *sql.Stmt
expungeMail *sql.Stmt
}
const mailsSchema = `
CREATE TABLE IF NOT EXISTS mails (
username TEXT NOT NULL,
mailbox TEXT NOT NULL,
id INTEGER NOT NULL DEFAULT 1,
mail BLOB NOT NULL,
datetime INTEGER NOT NULL,
seen BOOLEAN NOT NULL DEFAULT 0, -- the mail has been read
answered BOOLEAN NOT NULL DEFAULT 0, -- the mail has been replied to
flagged BOOLEAN NOT NULL DEFAULT 0, -- the mail has been flagged for later attention
deleted BOOLEAN NOT NULL DEFAULT 0, -- the email is marked for deletion at next EXPUNGE
PRIMARY KEY(username, mailbox, id),
FOREIGN KEY (username, mailbox) REFERENCES mailboxes(username, mailbox) ON DELETE CASCADE ON UPDATE CASCADE
);
DROP VIEW IF EXISTS inboxes;
CREATE VIEW IF NOT EXISTS inboxes AS SELECT * FROM (
SELECT ROW_NUMBER() OVER (PARTITION BY username, mailbox) AS seq, * FROM mails
)
ORDER BY username, mailbox, id;
`
const selectMailsStmt = `
SELECT * FROM inboxes
ORDER BY username, mailbox, id
`
const selectMailStmt = `
SELECT seq, id, mail, datetime, seen, answered, flagged, deleted FROM inboxes
WHERE username = $1 AND mailbox = $2 AND id = $3
ORDER BY username, mailbox, id
`
const selectMailCountStmt = `
SELECT COUNT(*) FROM mails WHERE username = $1 AND mailbox = $2
`
const selectMailUnseenStmt = `
SELECT COUNT(*) FROM mails WHERE username = $1 AND mailbox = $2 AND seen = 0
`
const searchMailStmt = `
SELECT id FROM mails
WHERE username = $1 AND mailbox = $2
ORDER BY username, mailbox, id
`
const insertMailStmt = `
INSERT INTO mails (username, mailbox, id, mail, datetime) VALUES(
$1, $2, (
SELECT IFNULL(MAX(id)+1,1) AS id FROM mails
WHERE username = $1 AND mailbox = $2
), $3, $4
)
RETURNING id;
`
const selectIDForSeqStmt = `
SELECT id FROM inboxes
WHERE username = $1 AND mailbox = $2 AND seq = $3
`
const selectMailNextID = `
SELECT IFNULL(MAX(id)+1,1) AS id FROM mails
WHERE username = $1 AND mailbox = $2
`
const updateMailFlagsStmt = `
UPDATE mails SET seen = $1, answered = $2, flagged = $3, deleted = $4 WHERE username = $5 AND mailbox = $6 AND id = $7
`
const deleteMailStmt = `
UPDATE mails SET deleted = 1 WHERE username = $1 AND mailbox = $2 AND id = $3
`
const expungeMailStmt = `
DELETE FROM mails WHERE username = $1 AND mailbox = $2 AND deleted = 1
`
func NewTableMails(db *sql.DB) (*TableMails, error) {
t := &TableMails{
db: db,
}
_, err := db.Exec(mailsSchema)
if err != nil {
return nil, fmt.Errorf("db.Exec: %w", err)
}
t.selectMails, err = db.Prepare(selectMailsStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailsStmt): %w", err)
}
t.selectMail, err = db.Prepare(selectMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailStmt): %w", err)
}
t.selectMailNextID, err = db.Prepare(selectMailNextID)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailNextID): %w", err)
}
t.selectIDForSeq, err = db.Prepare(selectIDForSeqStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectPIDForIDStmt): %w", err)
}
t.searchMail, err = db.Prepare(searchMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectPIDForIDStmt): %w", err)
}
t.createMail, err = db.Prepare(insertMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(insertMailStmt): %w", err)
}
t.updateMailFlags, err = db.Prepare(updateMailFlagsStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(updateMailSeenStmt): %w", err)
}
t.deleteMail, err = db.Prepare(deleteMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(deleteMailStmt): %w", err)
}
t.expungeMail, err = db.Prepare(expungeMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(expungeMailStmt): %w", err)
}
t.countMails, err = db.Prepare(selectMailCountStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailCountStmt): %w", err)
}
t.countUnseenMails, err = db.Prepare(selectMailUnseenStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailUnseenStmt): %w", err)
}
return t, nil
}
func (t *TableMails) MailCreate(user, mailbox string, data []byte) (int, error) {
var id int
err := t.createMail.QueryRow(user, mailbox, data, time.Now().Unix()).Scan(&id)
return id, err
}
func (t *TableMails) MailSelect(user, mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error) {
var data []byte
var seen, answered, flagged, deleted bool
var ts int64
var seq, pid int
err := t.selectMail.QueryRow(user, mailbox, id).Scan(&seq, &pid, &data, &ts, &seen, &answered, &flagged, &deleted)
return seq, pid, data, seen, answered, flagged, deleted, time.Unix(ts, 0), err
}
func (t *TableMails) MailSearch(user, mailbox string) ([]uint32, error) {
var ids []uint32
rows, err := t.searchMail.Query(user, mailbox)
if err != nil {
return nil, fmt.Errorf("t.searchMail.Query: %w", err)
}
defer rows.Close()
for rows.Next() {
var id uint32
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("rows.Scan: %w", err)
}
ids = append(ids, id)
}
return ids, nil
}
func (t *TableMails) MailNextID(user, mailbox string) (int, error) {
var id int
err := t.selectMailNextID.QueryRow(user, mailbox).Scan(&id)
return id, err
}
func (t *TableMails) MailIDForSeq(user, mailbox string, seq int) (int, error) {
var id int
err := t.selectIDForSeq.QueryRow(user, mailbox, seq).Scan(&id)
return id, err
}
func (t *TableMails) MailUnseen(user, mailbox string) (int, error) {
var unseen int
err := t.countUnseenMails.QueryRow(user, mailbox).Scan(&unseen)
return unseen, err
}
func (t *TableMails) MailUpdateFlags(user, mailbox string, id int, seen, answered, flagged, deleted bool) error {
_, err := t.updateMailFlags.Exec(seen, answered, flagged, deleted, user, mailbox, id)
return err
}
func (t *TableMails) MailDelete(user, mailbox, id string) error {
_, err := t.deleteMail.Exec(user, mailbox, id)
return err
}
func (t *TableMails) MailExpunge(user, mailbox string) error {
_, err := t.expungeMail.Exec(user, mailbox)
return err
}
func (t *TableMails) MailCount(user, mailbox string) (int, error) {
var count int
err := t.countMails.QueryRow(user, mailbox).Scan(&count)
return count, err
}

View File

@@ -0,0 +1,76 @@
package sqlite3
import (
"database/sql"
"fmt"
"golang.org/x/crypto/bcrypt"
)
type TableUsers struct {
db *sql.DB
getPassword *sql.Stmt
insertUser *sql.Stmt
}
const usersSchema = `
CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL,
password TEXT NOT NULL,
PRIMARY KEY(username)
);
`
const usersGetPassword = `
SELECT password FROM users WHERE username = $1
`
const usersInsertUser = `
INSERT INTO users (username, password) VALUES($1, $2)
`
func NewTableUsers(db *sql.DB) (*TableUsers, error) {
t := &TableUsers{
db: db,
}
_, err := db.Exec(usersSchema)
if err != nil {
return nil, fmt.Errorf("db.Exec: %w", err)
}
t.getPassword, err = db.Prepare(usersGetPassword)
if err != nil {
return nil, fmt.Errorf("db.Prepare(getPassword): %w", err)
}
t.insertUser, err = db.Prepare(usersInsertUser)
if err != nil {
return nil, fmt.Errorf("db.Prepare(usersInsertUser): %w", err)
}
return t, nil
}
func (t *TableUsers) CreateUser(username, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("bcrypt.GenerateFromPassword: %w", err)
}
if _, err := t.insertUser.Exec(username, hash); err != nil {
return fmt.Errorf("t.insertUser.Exec: %w", err)
}
return nil
}
func (t *TableUsers) TryAuthenticate(username, password string) (bool, error) {
var dbPasswordHash []byte
err := t.getPassword.QueryRow(username).Scan(&dbPasswordHash)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
err = bcrypt.CompareHashAndPassword(dbPasswordHash, []byte(password))
if err == nil {
return true, nil
}
return false, err
}

View File

@@ -0,0 +1,28 @@
package storage
import "time"
type Storage interface {
ConfigGet(key string) (string, error)
ConfigSet(key, value string) error
TryAuthenticate(username, password string) (bool, error)
MailboxSelect(user, mailbox string) (bool, error)
MailNextID(user, mailbox string) (int, error)
MailIDForSeq(user, mailbox string, id int) (int, error)
MailUnseen(user, mailbox string) (int, error)
MailboxList(user string, onlySubscribed bool) ([]string, error)
MailboxCreate(user, name string) error
MailboxRename(user, old, new string) error
MailboxDelete(user, name string) error
MailboxSubscribe(user, name string, subscribed bool) error
MailCreate(user, mailbox string, data []byte) (int, error)
MailSelect(user, mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error)
MailSearch(user, mailbox string) ([]uint32, error)
MailUpdateFlags(user, mailbox string, id int, seen, answered, flagged, deleted bool) error
MailDelete(user, mailbox, id string) error
MailExpunge(user, mailbox string) error
MailCount(user, mailbox string) (int, error)
}

View File

@@ -0,0 +1,10 @@
package transport
import (
"net"
)
type Transport interface {
Dial(host string) (net.Conn, error)
Listener() net.Listener
}

View File

@@ -0,0 +1,76 @@
package transport
import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"log"
"net"
iwt "github.com/Arceliar/ironwood/types"
gologme "github.com/gologme/log"
"github.com/neilalexander/utp"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
)
type YggdrasilTransport struct {
Sessions *utp.Socket
}
func NewYggdrasilTransport(log *log.Logger, sk ed25519.PrivateKey, pk ed25519.PublicKey, peer string) (*YggdrasilTransport, error) {
config := &config.NodeConfig{
PublicKey: hex.EncodeToString(pk),
PrivateKey: hex.EncodeToString(sk),
MulticastInterfaces: []config.MulticastInterfaceConfig{
{
Regex: ".*",
Beacon: true,
Listen: true,
},
},
NodeInfo: map[string]interface{}{
"name": "Yggmail",
},
}
if peer != "" {
config.Peers = append(config.Peers, peer)
}
glog := gologme.New(log.Writer(), "[ \033[33mYggdrasil\033[0m ] ", 0)
glog.EnableLevel("warn")
glog.EnableLevel("error")
glog.EnableLevel("info")
core := &core.Core{}
if err := core.Start(config, glog); err != nil {
return nil, fmt.Errorf("core.Start: %w", err)
}
multicast := &multicast.Multicast{}
if err := multicast.Init(core, config, glog, nil); err != nil {
return nil, fmt.Errorf("multicast.Init: %w", err)
}
if err := multicast.Start(); err != nil {
return nil, fmt.Errorf("multicast.Start: %w", err)
}
us, err := utp.NewSocketFromPacketConnNoClose(core)
if err != nil {
return nil, fmt.Errorf("utp.NewSocketFromPacketConnNoClose: %w", err)
}
return &YggdrasilTransport{
Sessions: us,
}, nil
}
func (t *YggdrasilTransport) Dial(host string) (net.Conn, error) {
addr := make(iwt.Addr, ed25519.PublicKeySize)
k, err := hex.DecodeString(host)
if err != nil {
return nil, err
}
copy(addr, k)
return t.Sessions.DialAddr(addr)
}
func (t *YggdrasilTransport) Listener() net.Listener {
return t.Sessions
}