Kaiy Belist 39759db7c2 - Inter-node Protocol: Implemented a custom communication protocol based on Protobuf structures.
- Bridge Node Support: Added functionality to manage mail (send/receive) on remote nodes via a bridge node. Full transparent management requires a private key on the bridge, while the remote node can operate with only a public key.
 - End-to-End Encryption (E2EE): Integrated E2EE using age. Encryption and decryption occur strictly at the bridge level; node administrators cannot access message content.
 - New Addressing Schema: Transitioned from nodepub@yggmail to userpub@nodepub.
 - Key Adaptation: Implemented ed25519 to x25519 coordinate adaptation, allowing signing keys to be used for encryption.

Changed
 - Address Encoding: Switched from hex16 to base32 for the address space.
 - Memory Optimization: Replaced direct memory allocations with streaming for mail processing to reduce RAM overhead.
 - Code Refactoring: Performed decomposition of the original inherited codebase to improve maintainability.

Fixed
 - Mail Updates: Fixed the mail retrieval mechanism to ensure near-instant updates for incoming messages.

Changes to be committed:
	modified:   .gitignore
	modified:   README.md
	modified:   cmd/yggmail/main.go
	modified:   go.mod
	modified:   go.sum
	new file:   internal/account/manager.go
	new file:   internal/account/session.go
	modified:   internal/imapserver/backend.go
	modified:   internal/imapserver/imap.go
	modified:   internal/imapserver/mailbox.go
	modified:   internal/imapserver/notify.go
	modified:   internal/imapserver/user.go
	new file:   internal/notify/notify.go
	new file:   internal/notify/remote/remote.go
	new file:   internal/remote/client.go
	new file:   internal/remote/handlers.go
	new file:   internal/remote/io.go
	new file:   internal/remote/server.go
	new file:   internal/remote/sign.go
	new file:   internal/remote/types/request.pb.go
	new file:   internal/remote/types/request.proto
	new file:   internal/remote/types/response.pb.go
	new file:   internal/remote/types/response.proto
	new file:   internal/shell/shell.go
	modified:   internal/smtpsender/sender.go
	modified:   internal/smtpserver/backend.go
	modified:   internal/smtpserver/session_local.go
	modified:   internal/smtpserver/session_remote.go
	modified:   internal/smtpserver/smtp.go
	new file:   internal/storage/filestore/filestore.go
	new file:   internal/storage/local/storage.go
	new file:   internal/storage/remote/mail.go
	new file:   internal/storage/remote/mailbox.go
	new file:   internal/storage/remote/queue.go
	new file:   internal/storage/remote/storage.go
	new file:   internal/storage/remote/watch.go
	modified:   internal/storage/sqlite3/sqlite3.go
	new file:   internal/storage/sqlite3/table_accounts.go
	modified:   internal/storage/sqlite3/table_mailboxes.go
	modified:   internal/storage/sqlite3/table_mails.go
	modified:   internal/storage/sqlite3/table_queue.go
	modified:   internal/storage/storage.go
	modified:   internal/storage/types/types.go
	modified:   internal/utils/address.go
	new file:   internal/utils/age/age.go
	new file:   internal/utils/age/bech32/bech32.go
	new file:   internal/utils/crypt.go
	new file:   internal/utils/e2ee/crypt.go
	new file:   internal/utils/e2ee/e2ee_test.go
	new file:   internal/utils/e2ee/msg.go
	new file:   internal/utils/x25519.go
	modified:   internal/welcome/welcome.go
2026-02-14 02:34:09 +05:00

346 lines
8.4 KiB
Go

/*
* (Original Copyright (c) 2021 Neil Alexander)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package imapserver
import (
"bufio"
"io"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend/backendutil"
"github.com/emersion/go-message/textproto"
"github.com/neilalexander/yggmail/internal/storage"
"github.com/neilalexander/yggmail/internal/storage/types"
"github.com/neilalexander/yggmail/internal/utils/e2ee"
)
type Mailbox struct {
storage storage.Storage
name string
user *User
}
func (mbox *Mailbox) Name() string {
return mbox.name
}
func (mbox *Mailbox) Info() (*imap.MailboxInfo, error) {
return &imap.MailboxInfo{
Attributes: []string{},
Delimiter: "/",
Name: mbox.name,
}, nil
}
func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
pk := mbox.user.username
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, _ := mbox.storage.MailCount(pk, mbox.name)
status.Messages = uint32(count)
case imap.StatusUidNext:
id, _ := mbox.storage.MailNextID(pk, mbox.name)
status.UidNext = uint32(id)
case imap.StatusUidValidity:
status.UidValidity = 1
case imap.StatusUnseen:
unseen, _ := mbox.storage.MailUnseen(pk, mbox.name)
status.Unseen = uint32(unseen)
}
}
return status, nil
}
func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error {
defer close(ch)
pk := mbox.user.username
ids, err := mbox.getIDsFromSeqSet(uid, seqSet)
if err != nil {
return err
}
for _, id := range ids {
mseq, m, err := mbox.storage.MailSelect(pk, mbox.name, int(id))
if err != nil {
continue
}
// n, _ := io.ReadAll(m.Body.(io.ReadSeeker))
// mbox.user.log.Printf("Body preview: \n\n %q \n\n", string(n))
// m.Body.(io.ReadSeeker).Seek(0, io.SeekStart)
m.Body, err = e2ee.DecryptFilter(m.Body.(io.ReadSeekCloser), mbox.user.session.Decrypt)
if err != nil {
mbox.user.log.Println("decrypter error: ", err)
return err
}
msg, err := mbox.fetchMessage(mseq, m, items)
if err != nil {
continue
}
ch <- msg
}
return nil
}
func (mbox *Mailbox) fetchMessage(seqNum int, m *types.Mail, items []imap.FetchItem) (*imap.Message, error) {
fetched := imap.NewMessage(uint32(m.ID), items)
fetched.SeqNum = uint32(seqNum)
fetched.Uid = uint32(m.ID)
if !mbox.needsBody(items) || m.Body == nil {
return mbox.fillMetadataOnly(fetched, m, items), nil
}
defer m.Body.Close()
br := bufio.NewReader(m.Body)
hdr, hdrErr := textproto.ReadHeader(br)
if hdrErr != nil {
return mbox.fillMetadataOnly(fetched, m, items), nil
}
for _, item := range items {
switch item {
case imap.FetchEnvelope:
fetched.Envelope, _ = backendutil.FetchEnvelope(hdr)
case imap.FetchBody, imap.FetchBodyStructure:
fetched.BodyStructure, _ = backendutil.FetchBodyStructure(hdr, br, item == imap.FetchBodyStructure)
case imap.FetchRFC822Size:
fetched.Size = uint32(m.Size)
case imap.FetchInternalDate:
fetched.InternalDate = m.Date
case imap.FetchFlags:
fetched.Flags = mbox.formatFlags(m)
default:
if section, err := imap.ParseBodySectionName(item); err == nil {
l, _ := backendutil.FetchBodySection(hdr, br, section)
fetched.Body[section] = l
}
}
}
return fetched, nil
}
func (mbox *Mailbox) needsBody(items []imap.FetchItem) bool {
for _, item := range items {
switch item {
case imap.FetchEnvelope, imap.FetchBody, imap.FetchBodyStructure:
return true
default:
if _, err := imap.ParseBodySectionName(item); err == nil {
return true
}
}
}
return false
}
func (mbox *Mailbox) fillMetadataOnly(dest *imap.Message, m *types.Mail, items []imap.FetchItem) *imap.Message {
for _, item := range items {
switch item {
case imap.FetchFlags:
dest.Flags = mbox.formatFlags(m)
case imap.FetchInternalDate:
dest.InternalDate = m.Date
case imap.FetchRFC822Size:
dest.Size = uint32(m.Size)
}
}
return dest
}
func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
id, err := mbox.storage.MailCreate(mbox.user.username, mbox.name, body, body.Len())
if err != nil {
return err
}
seen, answered, flagged, deleted := mbox.parseFlags(flags)
return mbox.storage.MailUpdateFlags(mbox.user.username, mbox.name, id, seen, answered, flagged, deleted)
}
func (mbox *Mailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, destName string) error {
pk := mbox.user.username
ids, err := mbox.getIDsFromSeqSet(uid, seqSet)
if err != nil {
return err
}
for _, id := range ids {
_, mail, err := mbox.storage.MailSelect(pk, mbox.name, int(id))
if err != nil {
continue
}
newID, err := mbox.storage.MailCreate(pk, destName, mail.Body, int(mail.Size))
mail.Body.Close()
if err == nil {
_ = mbox.storage.MailUpdateFlags(pk, destName, newID, mail.Seen, mail.Answered, mail.Flagged, mail.Deleted)
}
}
return nil
}
func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, op imap.FlagsOp, flags []string) error {
pk := mbox.user.username
ids, err := mbox.getIDsFromSeqSet(uid, seqSet)
if err != nil {
return err
}
for _, id := range ids {
_, mail, err := mbox.storage.MailSelect(pk, mbox.name, int(id))
if err != nil {
continue
}
mail.Body.Close() // Close body. Read Flags
seen, ans, flg, del := mbox.calculateNewFlags(mail, op, flags)
_ = mbox.storage.MailUpdateFlags(pk, mbox.name, int(id), seen, ans, flg, del)
}
return nil
}
// calculateNewFlags выносит логику битовых (флаговых) операций из общего цикла
func (mbox *Mailbox) calculateNewFlags(mail *types.Mail, op imap.FlagsOp, flags []string) (s, a, f, d bool) {
s, a, f, d = mail.Seen, mail.Answered, mail.Flagged, mail.Deleted
ns, na, nf, nd := mbox.parseFlags(flags)
switch op {
case imap.SetFlags:
s, a, f, d = ns, na, nf, nd
case imap.AddFlags:
if ns {
s = true
}
if na {
a = true
}
if nf {
f = true
}
if nd {
d = true
}
case imap.RemoveFlags:
if ns {
s = false
}
if na {
a = false
}
if nf {
f = false
}
if nd {
d = false
}
}
return
}
func (mbox *Mailbox) parseFlags(flags []string) (s, a, f, d bool) {
for _, flag := range flags {
switch flag {
case "\\Seen":
s = true
case "\\Answered":
a = true
case "\\Flagged":
f = true
case "\\Deleted":
d = true
}
}
return
}
func (mbox *Mailbox) formatFlags(mail *types.Mail) []string {
f := []string{}
if mail.Seen {
f = append(f, "\\Seen")
}
if mail.Answered {
f = append(f, "\\Answered")
}
if mail.Flagged {
f = append(f, "\\Flagged")
}
if mail.Deleted {
f = append(f, "\\Deleted")
}
return f
}
func (mbox *Mailbox) getIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]int32, error) {
var ids []int32
pk := mbox.user.username
for _, set := range seqSet.Set {
stop := set.Stop
if stop == 0 {
// In IMAP 0 = All
next, _ := mbox.storage.MailNextID(pk, mbox.name)
stop = uint32(next - 1)
}
for i := set.Start; i <= stop; i++ {
if !uid {
// Convert uid by db
pid, err := mbox.storage.MailIDForSeq(pk, mbox.name, int(i))
if err == nil {
ids = append(ids, int32(pid))
}
} else {
ids = append(ids, int32(i))
}
}
}
return ids, nil
}
func (mbox *Mailbox) SetSubscribed(subscribed bool) error {
return mbox.storage.MailboxSubscribe(mbox.user.username, mbox.name, subscribed)
}
func (mbox *Mailbox) Check() error { return nil }
func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) {
return mbox.storage.MailSearch(mbox.user.username, mbox.name)
}
func (mbox *Mailbox) Expunge() error {
return mbox.storage.MailExpunge(mbox.user.username, mbox.name)
}
func (mbox *Mailbox) MoveMessages(uid bool, seqset *imap.SeqSet, dest string) error {
pk := mbox.user.username
ids, err := mbox.getIDsFromSeqSet(uid, seqset)
if err != nil {
return err
}
for _, id := range ids {
if nid, err := mbox.storage.MailMove(pk, mbox.name, int(id), dest); err == nil {
// Delete mail from Outbox before send in Queue
if mbox.name == "Outbox" {
_ = mbox.storage.QueueDeleteDestinationForID(mbox.user.Username(), "Outbox", int(nid))
}
}
}
return nil
}