Enviar email sem enviar email¶
No post anterior eu comentei que fiz um formulário de contato pro blog e pra isso precisava me avisar quando chegasse mensagem. O mais óbvio seria mandar um email pra mim mesmo, mas por que enviar um email quando eu posso só salvar um arquivo num diretório?
Eu já conhecia por cima o maildir, que é um formato usado para armazenar emails num sistema de arquivos local, onde cada email é um arquivo, tem um layout específico de diretórios e regras para os nomes dos arquivos. A especificação do maildir é bem pequena e trata dos diretórios onde as mensagens devem ser salvas e como os nomes dos arquivos devem ser gerados. Já o formato usado no arquivo com com o email é definido pelas RFCs 5322, 2045, 2046 e 2047.
O maildir¶
A estrutura de diretórios do maildir são três diretórios: tmp
, cur
e new
.
O diretório tmp
é onde ficam os arquivos dos emails que ainda estão sendo
recebidos. Quando o arquivo é recebido ele tem que ser copiado para o diretório new
.
O cliente de emails avisa o usuário que existem novos emails. E quando o usuário
ler o email ele deve ser movido para o diretório cur
. O nome do arquivo deve ser
um nome único e quando movido de new
para cur
o nome deve ter adicionado
algumas informações no final.
Nota
Na especificação do maildir tem algumas sugestões para gerar o nome único, entre eles pegar algo de /dev/urandom e deixar o nome como o hexa do que foi pego em /dev/urandom. Basicamente pode-se usar um UUID4 (acho :P)
As informações adicionadas ao final do nome do arquivo depois de movido são basicamente informações sobre o status do email. A semântica da info é a seguinte:
Info começada com 1,
: Semântica experimental
Info começada com 2,
: Cada letra depois da vírgula é uma flag independente.
- As flags são:
P
(passed): O usuário encaminhou o email para alguémR
(replied): O usuário respondeu o emailS
(seen): O usuário viu o conteúdo do emailT
(trashed): Mensagem marcada como lixoD
(draft): Mensagem é um rascunhoF
(flagged): Uma tag definida pelo usuário
Então uma mensagem em um arquivo nomeado 22eb0cf0-b71c-4411-88a1-aef292007e58
quando
está em new
, depois de movido para cur
teria um nome mais ou menos assim
22eb0cf0-b71c-4411-88a1-aef292007e58:2,SR
.
O IMF¶
IMF é o internet message format que é um formato para transmissão de mensagens de texto em ASCII definido na RFC 5322 e estendido com a introdução de MIME tyes pela RFC 2045 (e relacionados) para utilização de outros conjuntos de caracteres e envio de outros tipos de conteúdo além de texto, como imagens e sons.
Mensagens são basicamente duas partes: o cabeçalho e o corpo. O cabeçalho pode ter várias
linhas, que são os campos do cabeçalho, no formato: NomeDoCampo: Valor\r\n
. Uma
linha em branco separa o cabeçalho do corpo. O corpo na especificação original era
somente uma mensagem em ASCII e com a introdução de MIME types o corpo pode ser
uma variedade de formatos, definidos num cabeçalho. Aqui vai um exemplo de uma
mensagem com um corpo multipart que pode conter vários tipos de mensagem no
mesmo corpo
Date: Thu, 28 Aug 2025 01:54:01 -0300
From: Juca <juca@poraodojuca>
To: Zé <ze@casadoze>
Subject: Olá
MIME-Version: 1.0
Message-ID: 1756357403.issodeveriaserumaidentificacao@poraodojuca
Content-Type: multipart/mixed; boundary="uma-string-que-marca-o-limite"
--uma-string-que-marca-o-limite
Content-Type: text/plain; charset="UTF-8"
Como vai, como vai, vai, vai?
--uma-string-que-marca-o-limite
Content-Type: text/html; charset="UTF-8"
<html><body><strong>
Como vai, como vai, vai, vai?
</strong></body></html>
--uma-string-que-marca-o-limite
Content-Type: image/jpeg
Content-Disposition: attachment; filename="foto.jpeg"
Content-Transfer-Encoding: base64
... aqui viria uma imagem jpeg em base64 ...
--uma-string-que-marca-o-limite--
O que acontece aí é o seguinte: A primeira parte, até a primeira linha em branco é o cabeçalho da mensagem e o restante depois da primeira linha em branco é o corpo, um corpo que tem 3 partes distintas, uma em texto puro, uma em html e uma imagem anexa.
Os campos do cabeçalho obrigatórios são o From
e o Date
, o Message-ID
apesar
de não ser obrigatório deveria estar presente em todas as mensagens. O formato de
Message-ID
é <timestamp>.<uma-string-identificadora>@<umhost>
. O
Content-Type: multipart/mixed; boundary="uma-string-que-marca-o-limite"
indica que o corpo da mensagem está dividido em várias partes, cada um com seu próprio
Content-Type. boundary
é uma string que separa uma parte do corpo de outra. Essa
string deve ser gerada de maneira que seja bem difícil ela esteja repetida no corpo
do email.
Eu não implementei tudo isso¶
Claro que isso é coisa demais só pro que eu precisava. O formulário de contato é só um campo de texto e pra fazer a parte do maildir já tinha o go-maildir. Então a minha implementaçãozinha meia-boca ficou mais ou menos assim:
// EmailMessage represents an email to be sent. Note that as this have
// no content type and the body is a string, only text/plain bodies are
// supported.
type EmailMessage struct {
From string
To []string
Subject string
Body string
Timestamp int64
}
// NewEmailMessage checks for missing from or to.
func NewEmailMessage(from string, to []string, subject string, body string) (EmailMessage, error) {
if from == "" {
return EmailMessage{}, errors.New("from can't be empty")
}
if to == nil || len(to) == 0 {
return EmailMessage{}, errors.New("to can't be empty")
}
ts := time.Now().Unix()
msg := EmailMessage{
From: from,
To: to,
Subject: subject,
Body: body,
Timestamp: ts,
}
return msg, nil
}
type keyGen func() (string, error)
// MaildirSender represents a maildir delivery
type MaildirSender struct {
MaildirPath string
keyGen keyGen
}
// SendEmail writes an EmailMessage to a local maildir
func (s MaildirSender) SendEmail(msg EmailMessage) error {
var d = maildir.Dir(s.MaildirPath)
err := initMaildir(d)
if err != nil {
return err
}
mformat, err := EmailMessage2Maildir(msg, s.keyGen)
if err != nil {
return err
}
del, err := maildir.NewDelivery(s.MaildirPath)
if err != nil {
return err
}
_, err = del.Write([]byte(mformat))
if err != nil {
return err
}
err = del.Close()
if err != nil {
return err
}
return nil
}
// NewMaildirSender returns a new NewMaildirSender instance
func NewMaildirSender(path string) MaildirSender {
s := MaildirSender{
MaildirPath: path,
keyGen: GenKey,
}
return s
}
// EmailMessage2Maildir converts an EmailMessage to a string in the
// maildir file format.
func EmailMessage2Maildir(msg EmailMessage, gen keyGen) (string, error) {
dtfmt := "Mon, 2 Jan 2006 15:04:05 -0700"
loc, err := time.LoadLocation("UTC")
if err != nil {
return "", err
}
dt := time.Unix(msg.Timestamp, 0).In(loc)
dtStr := dt.Format(dtfmt)
key, err := gen()
if err != nil {
return "", err
}
msgId := fmt.Sprintf("<%d.%s@localhost>", msg.Timestamp, key)
mformat := fmt.Sprintf("From: %s\n", msg.From)
toStr := strings.Join(msg.To, ",")
mformat += fmt.Sprintf("To: %s\n", toStr)
mformat += fmt.Sprintf("Subject: %s\n", msg.Subject)
mformat += fmt.Sprintf("Date: %s\n", dtStr)
mformat += fmt.Sprintf("Message-ID: %s\n", msgId)
mformat += fmt.Sprintf("MIME-Version: 1.0\n")
mformat += fmt.Sprintf("Content-Type: text/plain; charset=\"UTF-8\"\n")
mformat += "\n"
mformat += msg.Body
return mformat, nil
}
Mais pra frente do post vai ficar claro que eu não precisava do go-maildir, mas na hora foi isso o que eu fiz e boas, ficou! Agora é só meter um rsync pra pegar esses arquivos pra minha máquina e foi.
Claro que tinha um bug no Kmail¶
O Kmail é um cliente de mail pro KDE que tem muitos anos que eu sempre tento dar uma chance. Fui dar mais uma chance com o maildir, e claro que tinha um bug. Então chegou a hora da jigajoga!
A coisa é assim: na minha máquina local eu tenho uma estrutura de diretórios do maildir, com
new
, cur
e tmp
, assim o Kmail já reconhece esse diretório como um diretório que
vai receber emails. Aí eu faço um rsync do diretório new
do servidor para o diretório
new
na minha máquina e movo as menagens para um diretório de backup no servidor.
Nota
Aqui que a coisa fica clara que eu não precisava do maildir no servidor, era só jogar o arquivo num diretório qualquer, mas enfim… Burrice nunca falta no estoque.
O bug é que depois que eu fazia o rsync dos emails não atualizava o Kmail automaticamente, eu precisava clicar em «Atualizar» pra receber uma notificação. Então pra finalizar, depois do rsync, a gente dá um chute no Kmail pra ele acordar.
#!/bin/bash
LOCALDIR=~/somewhere/new/
REMOTEDIR='/somewhere/new/'
REMOTEDIR_CUR='/somewhere/cur/'
rsync -avz --progress eu@meuservidor:$REMOTEDIR $LOCALDIR --rsync-path="sudo rsync"
ssh eu@meuservidor "sudo find $REMOTEDIR -maxdepth 1 -type f -exec mv {} $REMOTEDIR_CUR \;"
# aqui a gente usa o dbus e pega todos os recursos que estão vinculados ao Akonadi
qdbus org.freedesktop.Akonadi /ResourceManager org.freedesktop.Akonadi.ResourceManager.resourceInstances |
while read resource;
do
if [[ "$resource" == *"maildir"* ]]; then
# se for maildir, a gente manda sincronizar a coisa
SERVICE_DBUS="org.freedesktop.Akonadi.Resource.${resource}"
OBJECT_PATH="/"
METHOD_NAME="org.freedesktop.Akonadi.Resource.synchronize"
qdbus $SERVICE_DBUS $OBJECT_PATH $METHOD_NAME
fi
done
E agora sim, envio e recebo emails sem enviar emails!