[譯] 使用 Django 構建一個簡單的郵件服務

當咱們建立一個 web 應用時,常常會有發送郵件的須要。不管是用戶註冊,仍是用戶忘記密碼,又或者是用戶下單後進行付款確認,都須要發送不一樣的郵件。因此發送郵件的需求其實是很是重要的,並且若是不在一開始就構建結構清晰的郵件服務,到後面可能會是一團糟。html

這篇文章不只會教你如何定義一個能輕鬆切換髮送平臺的電子郵件服務,而且還能夠對實際的郵件內容進行定製。前端

本文中使用 Django 來作出示例,但我但願你能夠將其中主要的思路應用到其餘你可能正在使用的語言或者框架中。python

讓咱們開始吧!android

基本的郵件發送

假設咱們但願在用戶註冊到咱們的 web 應用後向他發送郵件。咱們能夠參照 Django 文檔,向驗證經過並建立成功的用戶發送郵件。具體實現以下:ios

import logging

from rest_framework.views import APIView
from django.http import JsonResponse
from django.core.mail import send_mail

from users.models import User

logger = logging.getLogger('django')


class RegisterView(APIView):

    def post(self, request):
        # Run validations
        if not request.data:
            return JsonResponse({'errors': 'User data must be provided'}, status=400)
        if User.objects.filter(email=request.data['email']).exists():
            return JsonResponse({'errors': 'Email already in use'}, status=400)
        try:
            # Create new user
            user = User.objects.create_user(email=request.data['email'].lower())
            user.set_password(request.data['password'])
            user.save()
            
            # Send welcome email
            send_mail(
                subject='Welcome!',
                message='Hey there! Welcome to our platform.',
                html_message='<p><strong>Het there!</strong> Welcome to our platform.</p>'
                from_email='from@example.com',
                recipient_list=[user.email],
                fail_silently=False,
            )
            
            return JsonResponse({'status': 'ok'})
        except Exception as e:
            logger.error('Error at %s', 'register view', exc_info=e)
            return JsonResponse({'errors': 'Wrong data provided'}, status=400)
複製代碼

固然正如文檔中所說的那樣,你也必須提早設定好一些重要的配置項,好比 EMAIL_HOST 和 EMAIL_PORT。git

很好!如今咱們已經發送了歡迎郵件!github

建立一個 mailer 類

正如我以前所說,在咱們的應用中不一樣的模塊可能都須要發送郵件,因此最好有一個電子郵件服務或者 mailer 類來處理全部的郵件請求。這樣子會讓改動和迭代郵件的需求變得更簡單,由於咱們再也不須要每次都去翻遍所有代碼。web

import logging

from django.conf import settings
from django.core.mail import send_mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, to_email, subject, message, html_message):
        self.to_email = to_email
        self.subject = subject
        self.message = message
        self.html_message = html_message

    def send_email(self):
        send_mail(
            subject=self.subject,
            message=self.message,
            html_message=self.html_message,
            from_email='from@example.com',
            recipient_list=[self.to_email],
            fail_silently=False,
        )
複製代碼

讓咱們來看看通過此次改變後,註冊服務的視圖層是什麼樣子:django

import logging

from rest_framework.views import APIView
from django.http import JsonResponse
from django.core.mail import send_mail

from users.models import User
from users.mailers import BasicMailer

logger = logging.getLogger('django')


class RegisterView(APIView):

    def post(self, request):
        # Run validations
        if not request.data:
            return JsonResponse({'errors': 'User data must be provided'}, status=400)
        if User.objects.filter(email=request.data['email']).exists():
            return JsonResponse({'errors': 'Email already in use'}, status=400)
        try:
            # Create new user
            user = User.objects.create_user(email=request.data['email'].lower())
            user.set_password(request.data['password'])
            user.save()
            
            # Send welcome email
            BasicMailer(to_email=user.email, 
                        subject='Welcome!', 
                        message='Hey there! Welcome to our platform.', 
                        html_message='<p><strong>Het there!</strong> Welcome to our platform.</p>').send_email()
            
            return JsonResponse({'status': 'ok'})
        except Exception as e:
            logger.error('Error at %s', 'register view', exc_info=e)
            return JsonResponse({'errors': 'Wrong data provided'}, status=400)
複製代碼

mailer 子類

如今咱們已經把全部的「郵件代碼」移動到一個單獨的地方,能夠把它利用起來啦!這時候就能繼續建立特定的 mailer 類,並讓它們知道每次被調用時該發送什麼內容。如今讓咱們建立一個 mailer 類用來在每次用戶註冊時進行調用,另外一個 mailer 類用來發送訂單的確認信息。後端

import logging

from django.conf import settings
from django.core.mail import send_mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, to_email, subject, message, html_message):
        self.to_email = to_email
        self.subject = subject
        self.message = message
        self.html_message = html_message

    def send_email(self):
        send_mail(
            subject=self.subject,
            message=self.message,
            html_message=self.html_message,
            from_email='from@example.com',
            recipient_list=[self.to_email],
            fail_silently=False,
        )
        
        
class RegisterMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email,  
                         subject='Welcome!', 
                         message='Hey there! Welcome to our platform.', 
                         html_message='<p><strong>Het there!</strong> Welcome to our platform.</p>')
        

class NewOrderMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email,  
                         subject='New Order', 
                         message='You have just created a new order', 
                         html_message='<p>You have just created a new order.</p>')
複製代碼

這代表在不一樣的場景下集成郵件服務是很是簡單的。你只須要構造一個基礎的 mail 類來進行實現,而後在子類中設置具體內容。

由於不用實現全部郵件相關的代碼,因此如今咱們註冊服務的視圖層看起來更加簡明:

import logging

from rest_framework.views import APIView
from django.http import JsonResponse
from django.core.mail import send_mail

from users.models import User
from users.mailers import RegisterMailer

logger = logging.getLogger('django')


class RegisterView(APIView):

    def post(self, request):
        # Run validations
        if not request.data:
            return JsonResponse({'errors': 'User data must be provided'}, status=400)
        if User.objects.filter(email=request.data['email']).exists():
            return JsonResponse({'errors': 'Email already in use'}, status=400)
        try:
            # Create new user
            user = User.objects.create_user(email=request.data['email'].lower())
            user.set_password(request.data['password'])
            user.save()
            
            # Send welcome email
            RegisterMailer(to_email=user.email).send_email()
            
            return JsonResponse({'status': 'ok'})
        except Exception as e:
            logger.error('Error at %s', 'register view', exc_info=e)
            return JsonResponse({'errors': 'Wrong data provided'}, status=400)
複製代碼

使用 Sendgrid

假設咱們必須使用正式的 python 庫 將咱們的郵件服務後端遷移 Sendgrid (一個用於交易和營銷郵件的客戶通訊平臺)。咱們將不能再使用 Django 的 send_email 方法,並且咱們還不得不使用新庫的語法。嗯,但咱們仍是很幸運地!由於咱們已經將全部與郵件管理相關的代碼都放到了一個單獨的地方,因此咱們能夠很輕鬆的作出此次改動 😉

import logging

from django.conf import settings
from sendgrid import SendGridAPIClient, Email, Personalization
from sendgrid.helpers.mail import Mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, email, subject, template_id):
        self.mail = Mail()
        self.subject = subject
        self.template_id = template_id

    def create_email(self):
        self.mail.from_email = Email(settings.FROM_EMAIL)
        self.mail.subject = self.subject
        self.mail.template_id = self.template_id
        personalization = Personalization()
        personalization.add_to(Email(self.user.email))
        self.mail.add_personalization(personalization)

    def send_email(self):
        self.create_email()
        try:
            sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
            sg.send(self.mail)
        except Exception as e:
            logger.error('Error at %s', 'mailer', exc_info=e)
            

class RegisterMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='Welcome!', template_id=1234)

        
class NewOrderMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='New Order', template_id=5678)
複製代碼

請注意你必須在配置文件中設置 Sendgrid 的 api 密鑰,以及指定須要被使用的模板的 ID,Sendrid 會直接從本身的頁面中根據這個 ID 來加載指定的 html 郵件模版。

太好了!這並不困難,並且咱們不用去修改發送郵件的每一行代碼。

如今讓咱們的步子再邁大一點。

根據域信息定製郵件內容

固然咱們發送郵件的時候,有時候可能也會使用一些域信息來填充模板。好比說,若是在歡迎郵件裏面能有新用戶的名字,那確定會顯得更友好。Sendgrid 容許你在郵件模板中定義變量,這些變量將替換爲從咱們這裏接收的實際信息。因此如今讓咱們來添加這部分數據吧!

import logging

from django.conf import settings
from sendgrid import SendGridAPIClient, Email, Personalization
from sendgrid.helpers.mail import Mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, email, subject, template_id):
        self.mail = Mail()
        self.user = User.objects.get(email=email)
        self.subject = subject
        self.template_id = template_id
        self.substitutions = {
            'user_name': self.user.first_name,
            'user_surname': self.user.last_name
        }


    def create_email(self):
        self.mail.from_email = Email(settings.FROM_EMAIL)
        self.mail.subject = self.subject
        self.mail.template_id = self.template_id
        personalization = Personalization()
        personalization.add_to(Email(self.user.email))
        personalization.dynamic_template_data = self.substitutions
        self.mail.add_personalization(personalization)

    def send_email(self):
        self.create_email()
        try:
            sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
            sg.send(self.mail)
        except Exception as e:
            logger.error('Error at %s', 'mailer', exc_info=e)
            

class RegisterMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='Welcome!', template_id=1234)

        
class NewOrderMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='New Order', template_id=5678)
複製代碼

這裏我看到的惟一一個問題是替換方案不那麼靈活。極可能會發生的狀況是,咱們傳遞的數據多是用戶根據請求的上下文訪問不到的。好比說,新的訂單編號、重置密碼的連接等等。這些變量參數可能不少,把它們做爲命名參數傳遞可能會讓代碼變得比較髒亂。咱們但願的是一個基於關鍵字,而且長度可變的參數列表,通常來講會被定義爲 **kwargs,但在此咱們命名它爲 **substitutions,讓這個表達會更形象:

import logging

from django.conf import settings
from sendgrid import SendGridAPIClient, Email, Personalization
from sendgrid.helpers.mail import Mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, email, subject, template_id, **substitutions):
        self.mail = Mail()
        self.user = User.objects.get(email=email)
        self.subject = subject
        self.template_id = template_id
        self.substitutions = {
            'user_name': self.user.first_name,
            'user_surname': self.user.last_name
        }
        
        for key in substitutions:
            self.substitutions.update({key: substitutions[key]})

    def create_email(self):
        self.mail.from_email = Email(settings.FROM_EMAIL)
        self.mail.subject = self.subject
        self.mail.template_id = self.template_id
        personalization = Personalization()
        personalization.add_to(Email(self.user.email))
        personalization.dynamic_template_data = self.substitutions
        self.mail.add_personalization(personalization)

    def send_email(self):
        self.create_email()
        try:
            sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
            sg.send(self.mail)
        except Exception as e:
            logger.error('Error at %s', 'mailer', exc_info=e)
            

class RegisterMailer(BaseMailer):
    def __init__(self, to_email, **substitutions):
        super().__init__(to_email, subject='Welcome!', template_id=1234, **substitutions)

        
class NewOrderMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='New Order', template_id=5678, **substitutions)
複製代碼

若是但願將額外信息傳遞給 mailer 類,就須要按以下編碼:

NewOrderMailer(user.email, order_id=instance.id).send_email()
PasswordResetMailer(user.email, key=password_token.key).send_email()
複製代碼

總結

咱們已經建立了一個靈活的 mailer 類,它將全部與電子郵件相關的代碼封裝在一個單獨的地方,使代碼維護變得更容易,而且還接收可變的上下文參數來填充電子郵件內容!一會兒設計這整個方案確定會很困難,可是咱們一步一步的去實現就會簡單得多,而且會在這個過程當中受益良多。我鼓勵你基於這個設計,繼續開發郵件附件的功能!

很是感謝你閱讀這篇文章,我但願它能對你的項目有所幫助。也請關注我即將發佈的帖子,祝你人生幸運,編碼愉快。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索