Автоматическая обработка уведомлений SAS от Amazon для уведомлений о отказе и жалобах
Мы используем AWS SES для отправки писем. Amazon SES отправляет уведомления о отказе и жалобах через электронные письма или AWS SNS. Мы хотели бы автоматически обрабатывать уведомления о отказе и жалобах (исходящие из электронной почты или AWS SNS) для извлечения идентификаторов электронной почты, чтобы эти электронные письма можно было удалить из исходного списка.
Один из способов автоматизации - отправить эти уведомления в тему в AWS SNS, а затем подписаться на эту тему с помощью AWS SQS и, наконец, прочитать сообщения в AWS SQS. SNS поддерживает подписку на следующие протоколы: HTTP/HTTPS/EMail/EMail (JSON)/SMS/SQS. Это возможно, но я считаю это слишком громоздким для простой задачи автоматической обработки отказов и уведомлений о жалобах.
Есть ли элегантный способ решения этой проблемы?
Я нашел запись в блоге от Amazon с кодом на С#. Есть ли лучшее решение?
Ответы
Ответ 1
Я считаю, что прямой подпиской на SNS с использованием конечной точки HTTP является наиболее простой подход. Вам буквально приходится писать только несколько строк кода. Здесь мой пример django:
def process(request):
json = request.raw_post_data
js = simplejson.loads(json)
info = simplejson.loads(js["Message"])
type = info["notificationType"] # "Complaint" or "Bounce"
email = info["mail"]["destination"][0]
# do whatever you want with the email
Ответ 2
Я думаю, что вы описываете, возможно, самый элегантный способ. У вас уже есть очень подходящие сервисы в SNS и SQS, которые связаны с SDK на большинстве основных языков, что позволяет вам делать то, что вам нужно. Самая сложная часть - писать код для обновления/удаления записей в списках рассылки.
Ответ 3
Через пробную ошибку я придумал этот вариант - это для Django, но для меня достойная работа.
Сначала модели, затем обработчик запроса...
class ComplaintType:
ABUSE = 'abuse'
AUTH_FAILURE = 'auth-failure'
FRAUD = 'fraud'
NOT_SPAM = 'not-spam'
OTHER = 'other'
VIRUS = 'virus'
COMPLAINT_FEEDBACK_TYPE_CHOICES = [
[ComplaintType.ABUSE, _('Unsolicited email or some other kind of email abuse')],
[ComplaintType.AUTH_FAILURE, _('Unsolicited email or some other kind of email abuse')],
[ComplaintType.FRAUD, _('Some kind of fraud or phishing activity')],
[ComplaintType.NOT_SPAM, _('Entity providing the report does not consider the message to be spam')],
[ComplaintType.OTHER, _('Feedback does not fit into any other registered type')],
[ComplaintType.VIRUS, _('A virus was found in the originating message')]
]
class SES_Complaint(models.Model):
subject = models.CharField(max_length=255)
message = models.TextField()
email_address = models.EmailField(db_index=True)
user_agent = models.CharField(max_length=255)
complaint_feedback_type = models.CharField(max_length=255, choices=COMPLAINT_FEEDBACK_TYPE_CHOICES)
arrival_date = models.DateTimeField()
timestamp = models.DateTimeField()
feedback_id = models.CharField(max_length=255)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'SES Complaint'
verbose_name_plural = 'SES Complaints'
def get_reason(self):
return self.get_complaint_feedback_type_display()
class BounceType:
UNDETERMINED = 'Undetermined'
PERMANENT = 'Permanent'
TRANSIENT = 'Transient'
class BounceSubType:
UNDETERMINED = 'Undetermined'
GENERAL = 'General'
NO_EMAIL = 'NoEmail'
SUPPRESSED = 'Suppressed'
MAILBOX_FULL = 'MailboxFull'
MESSAGE_TOO_LARGE = 'MessageToolarge'
CONTENT_REJECTED = 'ContentRejected'
ATTACHMENT_REJECTED = 'AttachmentRejected'
BOUNCE_TYPE_CHOICES = [
[BounceType.UNDETERMINED, _('Unable to determine a specific bounce reason')],
[BounceType.PERMANENT, _('Unable to successfully send')],
[BounceType.TRANSIENT, _('All retry attempts have been exhausted')],
]
BOUNCE_SUB_TYPE_CHOICES = [
[BounceSubType.UNDETERMINED, _('Unable to determine a specific bounce reason')],
[BounceSubType.GENERAL, _('General bounce. You may be able to successfully retry sending to that recipient in the future.')],
[BounceSubType.NO_EMAIL, _('Permanent hard bounce. The target email address does not exist.')],
[BounceSubType.SUPPRESSED, _('Address has a recent history of bouncing as invalid.')],
[BounceSubType.MAILBOX_FULL, _('Mailbox full')],
[BounceSubType.MESSAGE_TOO_LARGE, _('Message too large')],
[BounceSubType.CONTENT_REJECTED, _('Content rejected')],
[BounceSubType.ATTACHMENT_REJECTED, _('Attachment rejected')]
]
class SES_Bounce(models.Model):
subject = models.CharField(max_length=255)
message = models.TextField()
bounce_type = models.CharField(max_length=255, choices=BOUNCE_TYPE_CHOICES)
bounce_sub_type = models.CharField(max_length=255, choices=BOUNCE_SUB_TYPE_CHOICES)
timestamp = models.DateTimeField()
feedback_id = models.CharField(max_length=255)
status = models.CharField(max_length=255)
action = models.CharField(max_length=255)
diagnostic_code = models.CharField(max_length=255)
email_address = models.EmailField(db_index=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True, db_index=True)
class Meta:
verbose_name = 'SES Bounce'
verbose_name_plural = 'SES Bounces'
def get_reason(self):
return '%s - %s' % (self.get_bounce_type_display(), self.get_bounce_sub_type_display())
И вот обработчик запроса:
@csrf_exempt
def aws_sns(request):
logger.debug('Incoming SNS')
if request.method == 'POST':
logger.debug('Incoming SNS is POST')
sns_message_type = request.META.get('HTTP_X_AMZ_SNS_MESSAGE_TYPE', None)
if sns_message_type is not None:
logger.debug('Incoming SNS - %s', sns_message_type)
json_body = request.body
json_body = json_body.replace('\n', '')
js = loads(json_body)
if sns_message_type == "SubscriptionConfirmation":
subscribe_url = js["SubscribeURL"]
logger.debug('Incoming subscription - %s', subscribe_url)
urllib.urlopen(subscribe_url)
elif sns_message_type == "Notification":
message = js.get("Message", None)
message = message.replace('\n', '')
message = loads(message)
notification_type = message.get("notificationType", None)
if notification_type == 'AmazonSnsSubscriptionSucceeded':
logger.debug('Subscription succeeded')
elif notification_type == 'Bounce':
logger.debug('Incoming bounce')
bounce = message['bounce']
bounce_type = bounce['bounceType']
bounce_sub_type = bounce['bounceSubType']
timestamp = bounce['timestamp']
feedback_id = bounce['feedbackId']
bounce_recipients = bounce['bouncedRecipients']
for recipient in bounce_recipients:
status = recipient.get('status')
action = recipient.get('action')
#diagnostic_code = recipient['diagnosticCode']
email_address = recipient['emailAddress']
SES_Bounce.objects.filter(email_address=email_address).delete()
SES_Bounce.objects.create(
message=message,
bounce_type=bounce_type,
bounce_sub_type=bounce_sub_type,
timestamp=timestamp,
feedback_id=feedback_id,
status=status,
action=action,
#diagnostic_code=diagnostic_code,
email_address=email_address
)
elif notification_type == 'Complaint':
logger.debug('Incoming complaint')
complaint = message['complaint']
user_agent = complaint.get('userAgent')
complaint_feedback_type = complaint.get('complaintFeedbackType')
arrival_date = complaint.get('arrivalDate')
timestamp = complaint['timestamp']
feedback_id = complaint['feedbackId']
recipients = complaint['complainedRecipients']
for recipient in recipients:
email_address = recipient['emailAddress']
SES_Complaint.objects.filter(email_address=email_address).delete()
SES_Complaint.objects.create(
#subject=subject,
message=message,
email_address=email_address,
user_agent=user_agent,
complaint_feedback_type=complaint_feedback_type,
arrival_date=arrival_date,
timestamp=timestamp,
feedback_id=feedback_id
)
else:
logger.exception('Incoming Notification SNS is not supported: %s', notification_type)
return HttpResponse()
else:
logger.exception('Incoming SNS did not have the right header')
for key, value in request.META.items():
logger.debug('Key: %s - %s', key, value)
else:
logger.exception('Incoming SNS was not a POST')
return HttpResponseBadRequest()
Ответ 4
Недавно я смог получить эту работу, используя конечную точку HTTP через SNS. Я использую python/django для использования уведомления. Перед обработкой уведомлений необходимо обработать подписное сообщение; вы можете прочитать о подписках в документации SNS.
Я думаю, если у вас есть небольшое приложение, которое не отправляет на многие электронные письма; Конечная точка http должна работать нормально. Этот код требует, чтобы у вас была создана модель уведомления.
#process an amazon sns http endpoint notification for amazon ses bounces and complaints
@csrf_exempt
def process_ses_notification(request):
if request.POST:
json_body = request.body
#remove this control character(throws an error) thats present inside the test subscription confirmation
js = loads(json_body.replace('\n', ''))
if js["Type"] == "SubscriptionConfirmation":
subscribe_url = js["SubscribeURL"]
urllib.urlopen(subscribe_url)
return HttpResponse(status=200)
elif js["Type"] == "Notification":
#process message from amazon sns
arg_info = loads(js["Message"]) # may need to use loads(js["Message"]) after testing with amazon
arg_notification_type = arg_info["notificationType"]
if arg_notification_type == 'Bounce':
#required bounce object fields
arg_emails=arg_info["bounce"]["bouncedRecipients"]
arg_notification_subtype=arg_info["bounce"]["bounceType"]
arg_feedback_id=arg_info["bounce"]["feedbackId"]
arg_date_recorded=arg_info["bounce"]["timestamp"]
elif arg_notification_type == 'Complaint':
#required complaint object fields
arg_emails=arg_info["complaint"]["complainedRecipients"]
arg_feedback_id=arg_info["complaint"]["feedbackId"]
arg_date_recorded=arg_info["complaint"]["timestamp"]
#check if feedback type is inside optional field name
if "complaintFeedbackType" in arg_info["complaint"]:
arg_notification_subtype=arg_info["complaint"]["complaintFeedbackType"]
else:
arg_notification_subtype=""
else:
HttpResponse(status=400)
#save notifications for multiple emails
for arg_email in arg_emails:
notification = SES_Notification(info=json_body, notification_type=arg_notification_type,
email=arg_email["emailAddress"], notification_subtype=arg_notification_subtype,
date_recorded=arg_date_recorded, feedback_id=arg_feedback_id)
notification.save()
return HttpResponse(status=200)
return HttpResponse(status=400)
Ответ 5
Это также легко сделать с обработкой стиля VERP. Я написал небольшое сообщение в блоге об этом... http://tb-it.blogspot.co.nz/2015/06/aws-ses-verp-and-hmailserver.html
Ответ 6
все приведенные выше ответы великолепны, но просто небольшое и важное дополнение:
вам сначала нужно проверить, что запрос от amazon sns: (как описано в Проверка подписи сообщений SNA Amazon)
для кода python, который проверяет подпись - хорошим примером является здесь