Skip to main content

EMail Forwarding with SES

SES 기본적인 내용#

  • AWS SES 는 AWS 이메일 서비스이다.
  • 아직 서울리전 서비스 안된다. 오레곤이나 버지니아리전 사용하면된다.
  • 기본 컨셉은 Route53을 통해 이메일을 SES로 라우트하고 받은 메시지를 S3, SNS, Lambda등으로 처리하는 방식

연습1 - S3버킷에 이메일 메시지 저장하기#

목표#

  • 00data.com으로 들어오는 이메일 메시지를 S3버킷에 저장하기

도메인 설정#

  • Route53에서 00data.com 도메인을 사용하도록 설정한다.
  • SES>Identity Manager>Domains에서 Verify New Domain
  • 00data.com 추가하고 저장한 다음 5분정도 기다리고 reload 하면 verified 된다.

규칙 rule 만들기#

  • SES>Email Receiving>Rule Sets에서 규칙 만들기
  • Active Rule Set이 있으면 그 밑에 New Rule 만든다.
  • Rule의 Action을 만들때 S3를 선택한다.
  • S3 Bucket을 만들거나 기존 bucket을 선택한다.
  • bucket은 write권한이 있어야 한다.

연습2 - 이메일 메시지 다른 도메인으로 전달하기#

목표#

  • 00data.com 으로 들어오는 이메일을 realgrid.com 도메인으로 전달하기
  • 이때 계정은 같은 계정으로 전달한다.
  • 계획 1
    • S3 bucket에 저장되는 rule에서 action이 실행될때 SNS를 호출할 수 있다.
    • SNS에서 Lambda 함수를 호출하고 이 Lambda함수에서 메일을 전달한다.
  • 계획 2
    • SES의 rule에서 직접 Lambda 함수를 호출한다.

계획1 Rule Action에 SNS 지정하기#

image

SNS를 통하지 않고 SES에서 직접 Lambda를 호출 할 수도 있다. 이때 event 에 들어오는 인자에서 record[0]에 들어 있는 메시지 객체는 ses가 된다. SNS에서는 'Sns` 이다.

계획 2 SES에서 Lambda함수 호출 rule 지정#

image

전달을 위한 Lambda함수 만들기#

  • event 형식으로 lambda함수를 만들면 handler에 전달되는 event 인자는 record 라는 이름의 object이다.
  • receiving-email-action-lambda 참조
  • event 방식은 비동기 처리된다.
  • 전달되는 메시는 receiving-email-notification-contents 참조
  • 이메일 메시지 가져오기
    • SNS를 통해 호출되는 함수는 event.Records[0].Sns.Message로 가져온다.
    • SES에서 직접 호출되는 함수는 event.Records[0].ses.mail로 가져온다. 이 경우 이메일 내용인 contents는 없다.

Email Forwarding Lambda 함수 코드#

const AWS = require('aws-sdk');
const forwarderMail = process.env.forwarder_mail;
const bucketName = process.env.bucket_name;
// s3에서 가져온 파일의 Body 속성은 { type: "", data: [] }의 형태로 되어 있다.
const s3FileToEmailString = (file) => {
const str = file.Body.reduce((accumulator, currentValue) => {
return accumulator + String.fromCharCode(currentValue);
});
return str;
};
// 대상 이메일 주소의 도메인을 forward 주소로 변경
const getForwardAddress = (to) => {
const id = to.substring(0, to.lastIndexOf('@'));
return id + '@' + process.env.forward_domain;
};
exports.handler = function (event, context) {
// DEBUG
// console.log(JSON.stringify(event));
// console.log(JSON.stringify(event.Records[0].ses));
const s3 = new AWS.S3();
const sesNotification = event.Records[0].ses;
// s3에서 이메일 메시지를 가져오는 이유는 150KB보다 큰 이메일 메시지는
// SNS를 통해 Notify되지 않기 때문이다.
s3.getObject(
{
Bucket: bucketName,
Key: sesNotification.mail.messageId,
},
function (err, file) {
if (err) context.fail(err);
else {
// DEBUG
// console.log(JSON.stringify(sesNotification.mail));
// console.log("Raw email:\n" + JSON.stringify(data));
// id + forwardDomain
const toAddress = getForwardAddress(
sesNotification.mail.commonHeaders.to[0]
);
let email = s3FileToEmailString(file);
let headers = 'From: ' + forwarderMail + ' \r\n';
headers +=
'Reply-To: ' + sesNotification.mail.commonHeaders.from[0] + '\r\n';
headers +=
'X-Original-To: ' + sesNotification.mail.commonHeaders.to[0] + '\r\n';
headers += 'To: ' + toAddress + '\r\n';
headers +=
'Subject: Fwd: ' +
sesNotification.mail.commonHeaders.subject +
'\r\n';
// console.log(email);
if (email) {
var res;
res = email.match(/Content-Type:.+\s*boundary.*/);
if (res) {
headers += res[0] + '\r\n';
} else {
res = email.match(/^Content-Type:(.*)/m);
if (res) {
headers += res[0] + '\r\n';
}
}
res = email.match(/^Content-Transfer-Encoding:(.*)/m);
if (res) {
headers += res[0] + '\r\n';
}
res = email.match(/^MIME-Version:(.*)/m);
if (res) {
headers += res[0] + '\r\n';
}
var splitEmail = email.split('\r\n\r\n');
splitEmail.shift();
email = headers + '\r\n' + splitEmail.join('\r\n\r\n');
} else {
email = headers + '\r\n' + 'Empty email';
}
// console.log(email)
const params = {
RawMessage: { Data: email },
};
new AWS.SES().sendRawEmail(params, function (err, data) {
if (err) context.fail(err);
else {
console.log('Sent with MessageId: ' + data.MessageId);
context.succeed();
}
});
}
}
);
};

Lambda함수를 실행하기 위한 정책과 역할 만들기#

  • IAM 새 정책 만들기: SESEmailForward 이름으로
    • SES : 쓰기에서 SendEmail, SendRawEmail 허용
    • CloudWatch Logs : 쓰기에서 CreateLogGroup, CreateLogStream, PutLogEvents 허용
  • IAM 새 역할 만들기 SESEmailForwardRole 이름으로
    • SESEmailForward 정책 포함
  • 위 함수에서 SESEmailForwardRole 역할선택

문제점#

문제점 1#

계획2 의 경우 즉, SES receiving rule에서 직접 Lambda를 호출 하여 event를 처리하는 경우 전달되는 메시지인 ses 객체에는 contents 가 포함되어 있지 않다. 따라서 메시지를 모두 전달하려면 계획1을 사용해야 한다.

문제점 2#

SNS 로 notify되는 메시지는 150KB 를 넘지 못한다. 따라서 메시지 크기가 150KB를 넘는 메시지를 전달하려면 S3에 저장된 메시지를 불러와서 전달하도록 함수를 작성해야 한다.

문제점 3#

이메일 메시지 크기가 크면 Lambda를 실행할때 기본 설정 시간인 3초 이상의 시간이 소요될 수 있다. 설정에서 시간을 좀 늘려 주어야 한다. 메시지 크기에 정확히 비례하는 것인지 확인 할 수는 없지만 1MB 크기의 메시지인 경우 약 7200ms 소요된다.

참고#

이 모든 시작은 https://github.com/AshanFernando/SESEmailForward/blob/master/forwarder.template 여기에서...