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

276 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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
}