Files
LambdaMail/services/protocols/internal/application/port/webmail_repository.go
T
Lucas RayanandClaude Sonnet 5.5 50a67531ba feat(webmail,auth): leitor estilo Outlook, responder a todos, importantes e isolamento do admin de dominio
Webmail
- Leitor de mensagem em componente proprio, com barra de comandos fixa,
  cabecalho compacto, faixa de imagens bloqueadas e chips de anexo.
- Divisor arrastavel entre lista e leitor (mouse, setas, duplo clique),
  largura persistida e limites que preservam os dois paineis.
- Destinatarios em chips (Para, Cc, Cco): aceita nomes com virgula,
  colagem de listas, validacao e bloqueio de envio com endereco invalido.
- Responder a todos, sem o proprio endereco e sem repetidos; In-Reply-To
  e References nas respostas; prefixo Re:/Fwd: sem repeticao.
- Marcar como importante (estrela, filtro, linha da lista) e enviar com
  alta importancia (Importance/X-Priority), com etiqueta no leitor.
- Sanitizador: a regexp de URI customizada descartava todo atributo que nao
  fosse link (width, colspan, bgcolor...), o que desmontava tabelas de
  e-mail; imagens de fundo agora sao bloqueadas de forma reversivel.

Protocols
- POST /api/v1/mail/flagged (\Flagged) com bump de modseq para IMAP.
- Renderizacao expoe message_id, in_reply_to e important.
- Cabecalhos de resposta validados contra injecao.

Auth
- As views do admin de dominio eram globais: dashboard, fila de saida,
  DMARC, preflight e diagnostico mostravam dados de todos os dominios.
  Agora todas respeitam o escopo da sessao; super admin segue vendo tudo.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 10:09:55 -03:00

104 lines
4.7 KiB
Go

package port
import (
"context"
"errors"
"time"
"github.com/google/uuid"
)
// WebmailFolder is a folder as the message list shows it.
type WebmailFolder struct {
ID string `json:"id"`
Name string `json:"name"`
SpecialUse string `json:"special_use"`
UnreadCount int `json:"unread_count"`
TotalCount int `json:"total_count"`
}
// WebmailMessage is one row of the message list.
//
// This is a read model projected straight from SQL rather than a hydrated
// aggregate, which is the CQRS exception PLAN.md section 2 carves out for
// webmail reads.
type WebmailMessage struct {
UID uint32 `json:"uid"`
Subject string `json:"subject"`
SenderAddress string `json:"sender_address"`
FromName string `json:"from_display_name"`
Snippet string `json:"snippet"`
SizeBytes int64 `json:"size_bytes"`
ReceivedAt time.Time `json:"received_at"`
HasAttachments bool `json:"has_attachments"`
Seen bool `json:"seen"`
Flagged bool `json:"flagged"`
Answered bool `json:"answered"`
SpamVerdict string `json:"spam_verdict"`
DmarcResult string `json:"dmarc_result"`
}
// WebmailMessageBody carries what is needed to render one message.
type WebmailMessageBody struct {
UID uint32 `json:"uid"`
BlobID uuid.UUID `json:"-"`
Raw []byte `json:"-"`
}
// WebmailRepository serves the webmail's read side.
//
// Every method takes the authenticated mailbox ID and scopes its query to it.
// Taking only a folder or a UID would let one account read another's mail by
// guessing an identifier, so the ownership check lives in the SQL rather than
// in the caller.
type WebmailRepository interface {
FindMailboxIDByAddress(ctx context.Context, address string) (string, error)
ListFolders(ctx context.Context, mailboxID string) ([]WebmailFolder, error)
// ListMessages returns the folder's messages newest first. search, when
// non-empty, filters on subject, sender and snippet.
ListMessages(ctx context.Context, mailboxID, folderName, search string, limit, offset int) ([]WebmailMessage, error)
// GetMessageBlob resolves one message to its stored blob, or returns
// ErrMessageNotFound when it does not belong to this mailbox.
GetMessageBlob(ctx context.Context, mailboxID, folderName string, uid uint32) (uuid.UUID, error)
// MarkSeen records that the user opened the message.
MarkSeen(ctx context.Context, mailboxID, folderName string, uid uint32, seen bool) error
// MarkFlagged sets or clears the \Flagged flag, which is what the webmail
// calls "important" and every IMAP client calls starred.
MarkFlagged(ctx context.Context, mailboxID, folderName string, uid uint32, flagged bool) error
// Expunge soft-deletes one message, keeping the folder counters in step.
Expunge(ctx context.Context, mailboxID, folderName string, uid uint32) error
// MoveToFolder relocates one message into a named folder and returns its
// new UID there. A UID is meaningful only inside one folder, so the
// message keeps its identity and its blob but is renumbered.
MoveToFolder(ctx context.Context, mailboxID, folderName string, uid uint32, target string) (uint32, error)
// MoveToTrash relocates one message into Trash and returns its new UID
// there. This is what deleting a message means everywhere but in Trash
// itself, where Expunge is the operation instead.
MoveToTrash(ctx context.Context, mailboxID, folderName string, uid uint32) (uint32, error)
}
// ErrFolderMissing reports a folder that is not there, or that is a standard
// folder and therefore not the user's to change.
var ErrFolderMissing = errors.New("folders: no such folder for this mailbox")
// ErrNoTrashFolder reports a mailbox with nowhere to put deleted mail, rather
// than the message being destroyed instead.
var ErrNoTrashFolder = errors.New("webmail: this mailbox has no Trash folder")
// ErrAlreadyInTrash tells the caller to expunge rather than move: a message
// already in Trash has no further destination.
var ErrAlreadyInTrash = errors.New("webmail: message is already in Trash")
// FolderAdmin manages the folders a mailbox owner creates for themselves.
//
// Separate from WebmailRepository because these are writes to the folder list
// itself rather than reads of the mail inside it, and because the reserved
// folders are protected in the use case above this, not here.
type FolderAdmin interface {
ListFolders(ctx context.Context, mailboxID string) ([]WebmailFolder, error)
CreateFolder(ctx context.Context, mailboxID, name string) error
RenameFolder(ctx context.Context, mailboxID, from, to string) error
// DeleteFolder removes the folder and the messages filed in it.
DeleteFolder(ctx context.Context, mailboxID, name string) error
}