mirror of
https://github.com/neilalexander/yggmail.git
synced 2026-05-17 00:56:30 +03:00
Initial commit
This commit is contained in:
33
internal/imapserver/backend.go
Normal file
33
internal/imapserver/backend.go
Normal 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
|
||||
}
|
||||
20
internal/imapserver/imap.go
Normal file
20
internal/imapserver/imap.go
Normal 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
|
||||
}
|
||||
293
internal/imapserver/mailbox.go
Normal file
293
internal/imapserver/mailbox.go
Normal 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)
|
||||
}
|
||||
75
internal/imapserver/user.go
Normal file
75
internal/imapserver/user.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user