ygggo/internal/remote/server.go

211 lines
6.3 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"
"context"
"crypto/ed25519"
"fmt"
"io"
"log"
"net"
"time"
"github.com/neilalexander/yggmail/internal/account"
"github.com/neilalexander/yggmail/internal/notify"
pb "github.com/neilalexander/yggmail/internal/remote/types"
"github.com/neilalexander/yggmail/internal/storage"
"github.com/neilalexander/yggmail/internal/utils"
)
type Server struct {
log *log.Logger
notify Notify
queue Queues
accMgr *account.Manager
storage storage.Storage
nodePublickey ed25519.PublicKey
validator *ReqValidator
}
type Notify interface {
Subscribe(pbk string) chan notify.Event
Unsubscribe(pbk string, ch chan notify.Event)
NotifyNew(pk string, ev notify.Event) error
}
type Queues interface {
QueueForServer(serverHex string) error
}
func NewRemoteServer(nodePublickey ed25519.PublicKey, s storage.Storage, m *account.Manager, l *log.Logger, notify Notify, queue Queues) *Server {
return &Server{
storage: s,
accMgr: m,
log: l,
notify: notify,
queue: queue,
nodePublickey: nodePublickey,
validator: NewValidator(time.Minute * 3),
}
}
func (srv *Server) Serve(l net.Listener) error {
srv.log.Println("Unified Remote Storage RPC server started (Streaming Mode)")
for {
conn, err := l.Accept()
if err != nil {
continue
}
// Serving in new gorutine
go srv.handleSession(conn)
}
}
func (srv *Server) handleSession(conn net.Conn) {
defer conn.Close()
br := bufio.NewReader(conn)
for {
conn.SetDeadline(time.Now().Add(30 * time.Second))
req, reqPayload, err := ReadRequest(br)
if err != nil {
if err != io.EOF {
srv.log.Printf("RPC: Session error: %v", err)
}
return
}
func() {
conn.SetDeadline(time.Now().Add(2 * time.Minute))
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Minute))
defer cancel()
resp, respPayload := srv.handleRequest(ctx, req, reqPayload)
err = SendFrame(conn, resp, respPayload)
if reqPayload != nil {
io.Copy(io.Discard, reqPayload.R)
reqPayload.Close()
}
if respPayload != nil {
respPayload.Close()
}
if err != nil {
srv.log.Printf("RPC: Send error: %v", err)
return
}
}()
}
}
func RespErr(err error) *pb.Response {
return &pb.Response{Success: false, Error: err.Error()}
}
func (srv *Server) handleRequest(ctx context.Context, req *pb.Request, reqPayload *Payload) (resp *pb.Response, respPayload *Payload) {
// srv.log.Printf("RPC: Request received from %s", req.AccountPk)
sPk, err := utils.DecodeString(req.AccountPk)
if err != nil {
return RespErr(fmt.Errorf("Error decode key")), respPayload
}
// if ok, err := VerifyRequest(req, sPk); err != nil || !ok {
// return nil, fmt.Errorf("401 Unauthorized: Signature mismatch")
// }
// srv.log.Printf("RPC: Request Verifed from %s", req.AccountPk)
ok, err := srv.validator.Validate(req, sPk)
if err != nil {
return RespErr(fmt.Errorf("401 Unauthorized: Signature mismatch")), respPayload
}
if !ok {
return &pb.Response{Success: true, Error: "duplicate request ignored"}, respPayload
}
// Check if this AccountPubKey belongs to a local account we manage
acc, _ := srv.accMgr.GetAccount(req.AccountPk)
isLocalAccount := (acc != nil)
_, isPushCall := req.Call.(*pb.Request_MailPush)
if !isLocalAccount && !isPushCall {
return RespErr(fmt.Errorf("403 Forbidden: Remote management not allowed")), respPayload
}
var rcname string
switch call := req.Call.(type) {
// --- Mailbox management (Local Account only via Policy) ---
case *pb.Request_MboxList:
resp, err = srv.handleMailboxList(ctx, req.AccountPk, call.MboxList)
rcname = "MboxList"
case *pb.Request_MboxCreate:
resp, err = srv.handleMailboxCreate(ctx, req.AccountPk, call.MboxCreate)
rcname = "MboxCreate"
case *pb.Request_MboxDelete:
resp, err = srv.handleMailboxDelete(ctx, req.AccountPk, call.MboxDelete)
rcname = "MboxDelete"
case *pb.Request_MboxRename:
resp, err = srv.handleMailboxRename(ctx, req.AccountPk, call.MboxRename)
rcname = "MboxRename"
case *pb.Request_MboxSub:
resp, err = srv.handleMailboxSubscribe(ctx, req.AccountPk, call.MboxSub)
rcname = "MboxSub"
case *pb.Request_MboxStatus:
resp, err = srv.handleMailboxStatus(ctx, req.AccountPk, call.MboxStatus)
rcname = "MboxStatus"
// --- Mail management ---
case *pb.Request_MailPush:
// handleMailPush has its own logic for Case A (Local) and Case B (Remote Node)
resp, err = srv.handleMailPush(ctx, req.AccountPk, call.MailPush, reqPayload)
rcname = "MailPush"
case *pb.Request_MailFetch:
resp, respPayload, err = srv.handleMailFetch(ctx, req.AccountPk, call.MailFetch)
rcname = "MailFetch"
case *pb.Request_MailFlags:
resp, err = srv.handleMailFlags(ctx, req.AccountPk, call.MailFlags)
rcname = "MailFlags"
case *pb.Request_MailMove:
resp, err = srv.handleMailMove(ctx, req.AccountPk, call.MailMove)
rcname = "MailMove"
case *pb.Request_MailDel:
resp, err = srv.handleMailDelete(ctx, req.AccountPk, call.MailDel)
rcname = "MailDel"
case *pb.Request_MailExpunge:
resp, err = srv.handleMailExpunge(ctx, req.AccountPk, call.MailExpunge)
rcname = "MailExpunge"
case *pb.Request_MailIdForSeq:
resp, err = srv.handleMailIdForSeq(ctx, req.AccountPk, call.MailIdForSeq)
rcname = "MailIdForSeq"
case *pb.Request_MailSearch:
resp, err = srv.handleMailSearch(ctx, req.AccountPk, call.MailSearch)
rcname = "MailSearch"
case *pb.Request_MailWatch:
resp, err = srv.handleMailWatch(ctx, req.AccountPk, call.MailWatch)
// --- Queue management (Local Account only via Policy) ---
case *pb.Request_QueueInsert:
resp, err = srv.handleQueueInsert(ctx, req.AccountPk, call.QueueInsert)
rcname = "QueueInsert"
default:
rcname = "Not Implemented"
resp = &pb.Response{Success: false, Error: "501 Not Implemented"}
}
// srv.log.Printf("RPC: %s : Authorized: %t: Publickey: %s :", rcname, isLocalAccount, req.AccountPk)
if err != nil {
srv.log.Printf("RPC Error: %s %v", rcname, err)
resp = &pb.Response{Success: false, Error: err.Error()}
}
return resp, respPayload
}