276 lines
5.9 KiB
Go
Raw Normal View History

- 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
/*
* Copyright (c) 2026 Kaiy Ragur
*
* 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 remote
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"sync"
pb "github.com/neilalexander/yggmail/internal/remote/types"
"google.golang.org/protobuf/proto"
)
var MagicHeader = []byte{0x59, 0x47, 0x47, 0x21} // "YGG!"
var MemoryLimit uint32 = 1024 * 1024 * 2 // 2 Mb
type Payload struct {
R io.ReadCloser
Size uint32
}
func (p *Payload) Close() error {
if p == nil || p.R == nil {
return nil
}
return p.R.Close()
}
type Tempfile struct {
io.ReadSeeker
file *os.File
size uint32
}
func (f *Tempfile) Len() int64 {
return int64(f.size)
}
func (f *Tempfile) Close() error {
if f.file != nil {
f.file.Close()
return os.Remove(f.file.Name())
}
return nil
}
func ReadRawData(reader io.Reader) (io.ReadCloser, int, error) {
peekBuf := new(bytes.Buffer)
peekBuf.Grow(int(MemoryLimit))
limited := io.LimitReader(reader, int64(MemoryLimit)+1)
written, err := peekBuf.ReadFrom(limited)
if err != nil && err != io.EOF {
return nil, 0, fmt.Errorf("failed to read data: %w", err)
}
if written < int64(MemoryLimit) {
return &Tempfile{
ReadSeeker: bytes.NewReader(peekBuf.Bytes()),
size: uint32(peekBuf.Len())}, peekBuf.Len(), nil
}
// Sew data that we readed and not-read
reader = io.MultiReader(peekBuf, reader)
// Read large mail to temp file
payload, err := os.CreateTemp("", "yggmail-*")
if err != nil {
return nil, 0, fmt.Errorf("failed to create temp-data file: %w", err)
}
size, err := io.Copy(payload, reader)
payload.Seek(0, 0)
return &Tempfile{payload, payload, uint32(size)}, int(size), nil
}
// func ReadRawData(r io.Reader, rawLen uint32) (io.ReadCloser, error) {
// if rawLen == 0 {
// return nil, nil
// }
//
// if rawLen <= MemoryLimit {
// // Write in memory
// buf := bytes.NewBuffer(make([]byte, 0, rawLen))
// _, err := io.CopyN(buf, r, int64(rawLen))
// return io.NopCloser(buf), err
// } else {
// // Write to tmp
// tmp, _ := os.CreateTemp("", "yggmail-payload-*")
// _, err := io.CopyN(tmp, r, int64(rawLen))
// tmp.Seek(0, 0) // Return in ahead
// return &Tempfile{File: tmp}, err
// }
// }
//
// Frame = Magic + uint32(Len Protobuff Meta) + uint32(Len Payload) + Protobuff + Payload
//
// Magic Header
//
// len(Protobuff)
// len()
//
// Protobuff Metadata
// Payload Rawdata
// # SendFrame
//
// Send Magic + uint32(Len Protobuff Meta) + uint32(Len Payload) + Protobuff + Payload
func readWithMagic(br *bufio.Reader, maxJunkBytes int) error {
magicIdx := 0
for range maxJunkBytes {
b, err := br.ReadByte()
if err != nil {
return err
}
if b == MagicHeader[magicIdx] {
magicIdx++
if magicIdx == len(MagicHeader) {
return nil // Found
}
} else {
if b == MagicHeader[0] {
magicIdx = 1
} else {
magicIdx = 0
}
}
}
return fmt.Errorf("sync failed: magic header not found")
}
// # ReadFrame
//
// Magic + uint32(Len Protobuff Meta) + uint32(Len Payload) + Protobuff + Payload
//
// Return Response / Request Reader
//
// RawData Reader
var marshalPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 1024*64) // Начинаем с 64КБ
},
}
type limitedReadCloser struct {
io.Reader
io.Closer
}
func ReadFrame(br *bufio.Reader, msg proto.Message) (proto.Message, *Payload, error) {
if err := readWithMagic(br, 1024); err != nil {
return nil, nil, err
}
var lengths [8]byte
if _, err := io.ReadFull(br, lengths[:]); err != nil {
return nil, nil, err
}
protoLen := binary.BigEndian.Uint32(lengths[0:4])
rawLen := binary.BigEndian.Uint32(lengths[4:8])
protoData := make([]byte, protoLen)
if _, err := io.ReadFull(br, protoData); err != nil {
return nil, nil, err
}
if err := proto.Unmarshal(protoData, msg); err != nil {
return nil, nil, err
}
var payload *Payload
if rawLen > 0 {
payload = &Payload{
Size: rawLen,
}
lr := io.LimitReader(br, int64(rawLen))
if rc, ok := lr.(io.ReadCloser); ok {
payload.R = &limitedReadCloser{lr, rc}
} else {
payload.R = io.NopCloser(lr)
}
}
return msg, payload, nil
}
func SendFrame(w io.Writer, msg proto.Message, payload *Payload) error {
if payload != nil && payload.R != nil {
defer payload.R.Close()
}
buf := marshalPool.Get().([]byte)
buf = buf[:0]
opts := proto.MarshalOptions{}
data, err := opts.MarshalAppend(buf, msg)
if err != nil {
marshalPool.Put(buf)
return err
}
defer marshalPool.Put(data)
pSize := uint32(0)
if payload != nil {
pSize = payload.Size
}
var header [12]byte
copy(header[0:4], MagicHeader)
binary.BigEndian.PutUint32(header[4:8], uint32(len(data)))
binary.BigEndian.PutUint32(header[8:12], pSize)
if _, err := w.Write(header[:]); err != nil {
return err
}
if _, err := w.Write(data); err != nil {
return err
}
if payload != nil && payload.Size > 0 {
if _, err := io.CopyN(w, payload.R, int64(pSize)); err != nil {
return err
}
}
return nil
}
func ReadRequest(r *bufio.Reader) (*pb.Request, *Payload, error) {
req := &pb.Request{}
_, payload, err := ReadFrame(r, req)
if err != nil {
return nil, nil, fmt.Errorf("ReadRequest: %w", err)
}
if payload == nil {
return req, nil, nil
}
payload.R, _, err = ReadRawData(payload.R)
if err != nil {
return nil, nil, fmt.Errorf("Error read payload")
}
return req, payload, nil
}
func ReadResponse(r *bufio.Reader) (*pb.Response, *Payload, error) {
resp := &pb.Response{}
_, payload, err := ReadFrame(r, resp)
if err != nil {
return nil, nil, fmt.Errorf("ReadRequest: %w", err)
}
// Unmarshal Protobuff metadata
// Read File
if payload == nil {
return resp, nil, nil
}
payload.R, _, err = ReadRawData(payload.R)
if err != nil {
return nil, nil, fmt.Errorf("Error read payload")
}
return resp, payload, nil
}