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

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
}