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

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
}