Envio de e-mails em HTML com o mailgun-go
Neste tutorial, vou demonstrar como você pode enviar e-mails em HTML com imagens incorporadas usando o mailgun-go. Antes de mergulharmos no código, vamos primeiro definir o espaço do problema e como podemos usar o Mailgun para melhorar a experiência do usuário da nossa aplicação.
Apresentando o Channel-stats
Channel-stats é um bot do Slack para coletar estatísticas sobre mensagens enviadas a um canal do Slack. Além de contabilizar emojis e links compartilhados no canal, ele também realiza a análise de sentimento das mensagens e fornece uma pontuação positiva ou negativa que pode posteriormente ser revisada pelos usuários e convertida em gráfico como uma porcentagem do total de mensagens.

Queremos expandir esse recurso com um relatório semanal por e-mail com as estatísticas de um canal para nossos usuários. Como o nosso e-mail incluirá dados em formato de gráfico, os e-mails em texto simples seriam bem monótonos; em vez disso, queremos enviar um e-mail em HTML rico com gráficos para a caixa de entrada do nosso usuário. Para fazer isso, precisamos criar um HTML com CSS inline e imagens para torná-lo visualmente atraente.
e-mail em HTML
Muito já foi escrito sobre o assunto do envio de HTML em e-mails, mas aqui estão algumas boas regras a seguir:
- SIM: use CSS inline
- SIM: use TABELAS em HTML para o layout
- SIM: use imagens (dê preferência ao formato .png)
- SIM: use imagens inline
- NÃO: use o HTML5
- NÃO: use animações em CSS
- NÃO: vincule a uma folha de estilos externa
- NÃO: use estilos CSS no HEAD
- NÃO: use JavaScript
- NÃO: use Flash
Muitas vezes, não basta seguir as regras acima, pois não há padrões estabelecidos sobre como o HTML em e-mails é renderizado. Se você tem o compromisso de garantir que seus e-mails sejam renderizados corretamente no maior número possível de clientes, pode ser uma boa ideia usar um serviço como o Litmus para criar, pré-visualizar e testar seu e-mail em diversos clientes. No entanto, para o nosso propósito, e como o channel-stats é um projeto de código aberto, vou manter os custos de produção baixos usando alguns modelos gratuitos fornecidos pelo Mailgun (recebi uma ajuda da nossa equipe de design de UX/UI). O resultado é o seguinte:

Agora que temos nosso HTML e CSS, precisamos colocar o CSS inline para que a maioria dos clientes de e-mail renderize o nosso e-mail corretamente. Há uma variedade de ferramentas on-line para fazer isso, mas recomendamos o Dialect Premailer para essa finalidade.
O código
Como queremos que o e-mail seja enviado semanalmente, usamos uma biblioteca cron para criar uma função que será executada todos os domingos à meia-noite. Em seguida, precisamos gerar as imagens que irão no nosso e-mail. O channel-stats já usa o go-chart para renderizar imagens de gráficos em .png para a UI, então podemos apenas adaptar isso para nossos propósitos.
Além disso, ao entregar o nosso projeto final, não queremos distribuir os arquivos HTML e CSS separadamente do binário Go final compilado; por isso, o channel-stats usa o projeto go-bindata para agrupar o HTML e o CSS em um único binário do channel-stats.
Agora, vamos dar uma olhada no código de renderização.
func NewReporter(conf Config, list ChanLister, notify Mailer, store Storer) (Reporter, error) {rn r := Report{rn log: GetLogger().WithField("prefix", "reporter"),rn cron: cron.New(),rn mail: notify,rn store: store,rn conf: conf,rn list: list,rn}rn return &r, r.start()rn}rnrnfunc (r *Report) start() error {rn err := r.cron.AddFunc(r.conf.Report.Schedule, func() {rn timeRange := toTimeRange(r.conf.Report.ReportDuration.Duration)rn r.log.Debugf("Creating report for %s to %s", timeRange.Start, timeRange.End)rnrn for _, channel := range r.list.Channels() {rn // Skip channels the bot is not inrn if !channel.IsMember {rn continuern }rnrn html, err := r.genHtml("html/templates/email.tmpl", channel.Name)rn if err != nil {rn r.log.Errorf("during email generate: %s", err)rn returnrn }rnrn data := ReportData{rn Images: make(map[string][]byte),rn Html: html,rn }rnrn // Generate the images for the reportrn data.Images["most-active.png"] = r.genImage(RenderSum, timeRange, channel.Id, "messages")rn data.Images["top-links.png"] = r.genImage(RenderSum, timeRange, channel.Id, "link")rn data.Images["top-emoji.png"] = r.genImage(RenderSum, timeRange, channel.Id, "emoji")rn data.Images["most-negative.png"] = r.genImage(RenderPercentage, timeRange, channel.Id, "negative")rn data.Images["most-positive.png"] = r.genImage(RenderPercentage, timeRange, channel.Id, "positive")rnrn // Email the reportrn if err := r.mail.Report(channel.Name, data); err != nil {rn r.log.Errorf("while sending report: %s", err)rn }rn }rn })rn if err != nil {rn return errrn }rnrn r.cron.Start()rn return nilrn}
No método Start(), nós iteramos sobre todos os canais dos quais o bot é membro e geramos um relatório para cada canal. Em seguida, fazemos uma chamada para genHtml(), que recupera nosso e-mail em HTML como um modelo chamado templates/email.tmpl do nosso armazenamento de ativos compilados no pacote HTML. Em seguida, executamos o modelo por meio do mecanismo de HTML/template padrão da linguagem Go para produzir o HTML final. Depois, a função genImage() chama a função de renderização com o intervalo de horas e o tipo de contador que queremos recuperar do armazenamento de dados. Assim que o ReportData estiver completo, repassamos os dados para mail.Report() para a entrega.
Agora que temos nossas imagens e o HTML, vamos fazer uma pausa para falar um pouco sobre MIME HTML e codificação de imagens. MIME é o formato no qual os corpos dos e-mails são codificados quando enviados por meio do protocolo SMTP. É o formato que permite aos clientes de e-mail codificar HTML, anexar e recuperar arquivos e imagens em um e-mail.
Para que nossas imagens sejam exibidas corretamente no HTML, precisamos codificar as imagens no MIME. Para isso, temos duas opções: podemos adicionar as imagens como um anexo, ou podemos colocar as imagens inline. A RFC sobre a disposição de conteúdo diz que o inline indica que a entidade deve ser exibida imediatamente ao usuário, ao passo que attachment significa que o usuário deve realizar uma ação adicional para visualizar a entidade. Como as nossas imagens devem ser exibidas imediatamente ao usuário por meio do HTML — optamos por usar a opção inline.
Neste ponto, poderíamos usar várias bibliotecas MIME para a linguagem Go para colocar nossas imagens inline e gerar o corpo do e-mail no formato MIME, mas com o Mailgun, não precisamos disso. O Mailgun gerará o MIME para nós e fornece opções para colocar arquivos inline e imagens por meio da API pública.
Agora que sabemos como colocar imagens inline no MIME, precisamos referenciá-las em nosso HTML. Para fazer isso, usamos o prefixo cid: em nossas tags . De forma que se a nossa imagem inline for chamada de most-active.png, nossa tag de imagem seria
Com o nosso HTML pronto, vamos ver como enviar o e-mail e as imagens com o mailgun-go.
func NewMailgunNotifier(conf Config) (Mailer, error) {rn return &Mailgun{rn mg: mailgun.NewMailgun(conf.Mailgun.Domain, conf.Mailgun.APIKey),rn log: GetLogger().WithField("prefix", "mailer"),rn conf: conf,rn }, nilrn}rnrn// Send a report to the designated email address (could be mailing list)rnfunc (m *Mailgun) Report(channelName string, data ReportData) error {rn if m.conf.Mailgun.ReportAddr == "" {rn m.log.Errorf("mailgun.enabled = true; however mailgun.report-address is empty; skipping..")rn return nilrn }rnrn // Create a subject for the reportrn subject := fmt.Sprintf("[channel-stats] Report for %s", channelName)rn // Create a message with no text bodyrn message := m.mg.NewMessage(m.conf.Mailgun.From, subject, "", m.conf.Mailgun.ReportAddr)rn // Send the HTML to mailgun for MIME encodingrn message.SetHtml(string(data.Html))rnrn for file, contents := range data.Images {rn message.AddReaderInline(file, ioutil.NopCloser(bytes.NewBuffer(contents)))rn }rnrn ctx, cancel := context.WithTimeout(context.Background(), m.conf.Mailgun.Timeout.Duration)rn defer cancel()rnrn _, id, err := m.mg.Send(ctx, message)rn if err != nil {rn return errrn }rn m.log.Infof("Sent report via mailgun (%s)", id)rn return nilrn}
Primeiro, criamos uma nova instância do Mailgun usando nosso nome de domínio e chave de API em NewMailgunNotifier(). A seguir, no método Report(), chamamos NewMessage() para criar um objeto ao qual adicionaremos o nosso HTML e as imagens. Observe que o argumento text para NewMessage() é uma string vazia. Embora seja possível codificar tanto o texto simples quanto o HTML na mensagem MIME, nós fornecemos apenas o HTML aqui porque as imagens de gráficos inline seriam inúteis para um cliente apenas de texto. Em seguida, chamamos o método SetHtml() e acrescentamos nossas imagens inline por meio de um objeto ReadCloser, que criamos dinamicamente a partir de nosso []byte buffer. Por fim, enviamos a solicitação elaborada à API do Mailgun para a construção e a entrega do MIME usando o método Send().
Conclusão
Esperamos que este tutorial tenha dado algumas dicas sobre como enviar e-mails baseados em HTML de alta qualidade usando o mailgun-go e a API do Mailgun. Se você tiver algum feedback ou encontrar bugs em algum dos projetos, o código completo e a biblioteca podem ser encontrados abaixo.
Tem interesse em trabalhar no Mailgun? Estamos contratando! E há várias vagas disponíveis para a equipe de desenvolvimento. Confira nossas vagas atuais aqui.
Webinar em destaque – Previsões & resoluções: envios em 2019
Perdeu este webinar no início de 2019? Não se preocupe, nós o gravamos! Assista novamente ao Nick e à Natalie falarem sobre algumas coisas que aconteceram com os e-mails em 2018 e o que eles achavam que estava por vir em 2019. As áreas técnica e de marketing chegaram a um meio-termo neste webinar, e você pode assistir a ele novamente aqui.

