EC2 인스턴스에서 실행 중인 postfix 서버가 있습니다. SES를 통해 모든 이메일을 내 개인 편지함으로 전달하고 싶습니다.
문제: AWS는 AWS 콘솔에서 확인된 FROM 주소만 허용하며 이 예의 FROM 주소는 twitter.com과 같은 모든 주소일 수 있습니다. 내 서버의 IP를 허용 목록에 추가하고 "발신자에 관계없이 이 위치에서 오는 모든 이메일을 수락합니다"라고 말할 수 없습니다(어쨌든 좋지 않은 생각입니다).
그래서 확인된 주소로 이메일을 전달할 수 있는 방법을 찾아야 하는데, 원래 보낸 사람의 주소를 잃어버리고 싶지 않습니다.
이를 수행할 수 있는 방법이 있습니까?
답변1
채팅에서 논의한 내용을 바탕으로 "발신자" 주소를 의도한 대로 변경한 다음 원래 목적지로 배송하되 "답장" 헤더를 추가하는 해킹된 맞춤 솔루션을 제공하겠습니다.
이것은 매우 해킹적인 접근 방식이지만~해야 한다실제로 PostFix를 통해 가야 할 곳으로 메시지를 보내기 전에 의도한 대로 메시지를 조작하십시오.
먼저 PostFix 포트를 변경해야 합니다.. 25
설정하려는 Python SMTP 핸들러가 해당 포트에서 작동하도록 Postfix SMTP 포트를 다른 포트로 변경해야 합니다 .
편집하다 /etc/postfix/master.cf
. 다음과 같은 줄을 찾습니다.
smtp inet n - y - - smtpd
이 줄을 주석 처리하고 그 아래에 다음을 사용합니다.
10025 inet n - y - - smtpd
이는 Postfix가 표준 SMTP 포트에서 수신 대기하는 것을 원하지 않는다는 것을 알려줍니다. 이 단계를 완료한 후 postfix 서비스를 다시 시작하십시오.
다음으로 Python SMTP 핸들러위에서 언급했습니다. 그러면 들어오는 메시지를 처리하고 조작한 후 시스템의 PostFix로 다시 보냅니다. 물론 이는 모든 메일이 로컬에서도 포트 25로 제출된다고 가정합니다.
이 코드는 다음에 존재합니다.GitHub의 GIST이는 내가 어딘가에서 찾은 일반 Python SMTP 서버 코드 예제를 기반으로 하며(하지만 어디서 왔는지 기억이 나지 않습니다!) 이에 대해 작업했습니다.
코드도 여기에 있습니다. 궁금하다면 Python 3으로 작성되었으며 대상 Python 버전으로 Python 3을 사용하여 작성되었습니다.
#!/usr/bin/env python3
# Libraries
import smtplib
import smtpd
import asyncore
import email
import sys
from datetime import datetime
print('Starting custom mail handling server...')
# We need to know where the SMTP server is heh.
SMTP_OUTBOUND = 'localhost'
# We also need to know what we want the "FROM" address to be
FROM_ADDR = "[email protected]"
DESTINATION_ADDRESS = "[email protected]"
#############
#############
# SMTP SERVER
#############
#############
# noinspection PyMissingTypeHints,PyBroadException
class AutoForwardHandlerSMTP(smtpd.SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
print('MESSAGE RECEIVED - [%s]' % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
print('Receiving message from:', peer)
print('Message addressed from:', mailfrom)
print('Message addressed to :', rcpttos)
print('Message length :', len(data))
print(data)
# Flush the output buffered (handy with the nohup launch)
sys.stdout.flush()
# Analyze and extract data headers
msg = email.message_from_string(data)
orig_from = ''
try:
orig_from = msg['From']
msg['Reply-To'] = orig_from
# We have to use 'replace header' methods to overwrite existing headers.
msg.replace_header("From", FROM_ADDR)
except:
print("Error manipulating headers:", sys.exc_info()[0])
conn = smtplib.SMTP(SMTP_OUTBOUND, 10025)
conn.sendmail(FROM_ADDR, msg["To"], msg.as_string())
# Flush the output buffered (handy with the nohup launch)
print("\n\n")
sys.stdout.flush()
return
# Listen to port 25 ( 0.0.0.0 can be replaced by the ip of your server but that will work with 0.0.0.0 )
server = AutoForwardHandlerSMTP(('0.0.0.0', 25), None)
# Wait for incoming emails
asyncore.loop()
/opt/PythonAutoForwarderSMTP.py
또는 원하는대로 저장하십시오 . 먼저 다음 명령을 루트로 실행하여(사용자 프롬프트를 통해 sudo
또는 사용자 프롬프트로 root
) 예상대로 작동하는지 확인합니다.
python3 /opt/PythonAutoForwarderSMTP.py
실행을 확인한 후 서버를 통해 이메일을 보냅니다. 이를 선택하여 메시지가 수신되고 처리되었음을 나타내는 스크립트의 로그 데이터를 제공해야 합니다. 그런 다음 Postfix 로그에도 연결이 표시되고 연결이 Postfix 이후 어딘가로 라우팅되고 있음을 볼 수 있습니다. 이 모든 것이 괜찮아 보이고 이메일을 올바르게 처리하고 있으며 이메일이 끝나는 다른 "보낸 사람" 주소로 이메일을 보고 있다면 이제 자동 시작되도록 할 수 있습니다! ( 계속하기 전에 Ctrl+를 눌러 Python 프로세스를 닫을 수 있습니다 C).
부팅 시 시작되기를 원한다고 가정하고 설정해야 합니다.
으로 root
실행 하고 crontab crontab -e
에 다음을 추가합니다 .root
@reboot /usr/bin/python3 /opt/PythonAutoForwarderSMTP.py 2>&1 >> /var/log/PythonSMTP.log &
crontab 파일을 저장합니다. 서버를 다시 시작하지 않으려면 방금 추가한 명령줄(해당 @reboot
부분 제외)을 실행하여 Python SMTP 처리기를 실행하세요.
실행 여부에 관계없이 cron
Python을 로드하는 프로세스는 결국 백그라운드로 분기되고 /var/log/PythonSMTP.log
추가 모드에서 모든 데이터 출력(Python 콘솔 또는 기타 오류)을 로그 파일에 넣습니다. 이렇게 하면 필요할 때마다 로그를 얻을 수 있습니다.
모든 것이 예상대로 작동하면 Reply-To 헤더가 올바르게 추가되고 메시지의 "From" 헤더가 예상대로 조정됩니다. 메시지가 서명된 경우 이것이 SPF 및 DKIM 검사에 대해 제대로 작동한다고 보장할 수는 없지만 Postfix를 사용하여 메시지를 다른 곳으로 전달하기 전에 메시지를 적절하게 "전처리"할 것이라고 말할 수 있습니다.
필수 보안 문제 및 기능 변경 알림:
- 발신자 DKIM 확인이 실패할 수 있습니다.서명된 메시지가 조작될 때마다 DKIM 서명 확인이 실패합니다. 이는 보낸 사람의 DKIM 서명이 손상되었을 수 있음을 의미합니다. 이건 뭔가 의미가 있어가능한서명 확인 실패로 인해 스팸으로 간주됩니다. 이 스크립트는 "그냥 작동"하도록 사용자 정의할 수 있지만 DKIM/SPF 검사를 위해 작성하지는 않았습니다.
- 이 Python SMTP 서버를 실행해야 합니다.
root
. 이는 기본적으로 Linux에서 수퍼유저가 아니면 1024 미만의 포트에 바인딩할 수 없기 때문에 필요합니다. 이것이 Postfix가 기본 "루트"가 소유한 프로세스를 갖고 실행이 루트로 실행되지 않는 이유입니다. 매우 긴 하위 프로세스(포트 바인딩에만 해당) .- 포트 25의 모든 메일은 결국 이 Python SMTP 서버를 통과하게 됩니다.. Postfix가 외부에서 내부로의 메일도 처리한다면 Python SMTP 서버가 그 자리를 대신하게 됩니다. 여기에는 몇 가지 단점이 있을 수 있지만 궁극적으로 원하는 것을 달성할 것입니다.
- 이것은 취약한 솔루션입니다.다른 솔루션만큼 취약하지는 않지만 Python 프로세스가 종료되면 자동으로 복구되지 않으므로 사례별로 오류를 처리해야 하며 때로는 오류가 발생하면 Python 프로세스를 다시 활성화해야 합니다. 발생합니다. 완전히 사라집니다.
- 가지다StartTLS 또는 SSL/TLS 핸들러가 없습니다.. 따라서 모든 것이 일반 텍스트입니다(안전하지 않습니다!).
항상 그렇듯이, 자신이 무엇을 하고 있는지 알지 못한다면 루트로 아무 것도 실행해서는 안 됩니다.이 경우에는 나처럼 보안 중심적이고 편집증적인 사람이라면 스크립트가 수행하는 작업과 루트로 실행할지 여부를 스스로 식별할 수 있도록 이 코드를 일반 보기로 제공했습니다. IT 보안 전문가는 물론 시스템 관리자 여러분도 꼭 숙지하시기 바랍니다.)
답변2
@Thomas Ward의 훌륭한 답변 외에도 AWS에는 매우 유사한 "선호되는"방식이 있습니다. 유일한 차이점은 외부 Python 스크립트가 아닌 AWS 내부 도구를 사용하여 작업을 수행한다는 것입니다.
이 방법과 다른 방법의 주요 차이점 중 하나는 이 방법은 바이러스/악성 프로그램 검사는 물론 DKIM 및 SPF 검사도 수행하므로 실제로 테스트하고 작동하는지 확인할 수 있다는 것입니다 PASS
.
그래서 저는 README
이 GitHub 저장소를 따라갔습니다.https://github.com/arithmetric/aws-lambda-ses-forwarder
이게 다 이 스크립트 때문이에요. 이를 AWS Lambda에 배치하면 SES 규칙에 따라 이메일을 사후 처리합니다.
다음은 설정 섹션의 사본입니다 README
.
노트: 등을 변경합니다 S3-BUCKET-NAME
.
config
index.js
SES에 저장된 이메일을 찾는 데 사용되는 S3 버킷 및 객체 접두사를 지정하려면 상단 객체의 값을 수정하세요 . 원래 대상에서 새 대상으로의 이메일 전달 매핑도 제공됩니다.AWS Lambda에서 새 함수를 추가하고 블루프린트 선택을 건너뜁니다.
함수 이름을 "SesForwarder"로 지정하고 선택적으로 설명을 제공합니다. 런타임이 Node.js 4.3 또는 6.10으로 설정되어 있는지 확인하세요.
Lambda 함수 코드의 경우 내용을 복사하여
index.js
인라인 코드 편집기에 붙여넣거나 리포지토리의 내용을 압축하여 직접 또는 S3를 통해 업로드하세요.핸들러가 로 설정되어 있는지 확인하세요
index.handler
.역할의 경우 새 역할 만들기에서 기본 실행 역할을 선택합니다. 팝업 창에서 역할에 이름(예: LambdaSesForwarder)을 지정합니다. 역할 정책을 다음과 같이 구성합니다.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*" }, { "Effect": "Allow", "Action": "ses:SendRawEmail", "Resource": "*" }, { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject" ], "Resource": "arn:aws:s3:::S3-BUCKET-NAME/*" } ] }
메모리는 128MB까지 예약할 수 있지만 안전상의 이유로 제한 시간은 10초로 설정되어 있습니다. 이 작업은 일반적으로 약 30MB와 몇 초 정도 걸립니다. 작업을 테스트한 후 시간 초과 제한을 줄일 수 있습니다.
AWS SES에서 이메일을 수신하고 전달하려는 도메인을 확인합니다. 또한 이메일 수신(또는 인바운드) SES 엔드포인트를 가리키도록 이러한 도메인에 대한 DNS MX 레코드를 구성합니다. 바라보다SES 문서 각 지역의 이메일 수신 엔드포인트용입니다.
SES에 대한 샌드박스 수준 액세스 권한이 있는 경우 이메일을 전달하려는 확인된 도메인에 없는 이메일 주소도 확인하세요.
인바운드 이메일 처리를 아직 구성하지 않은 경우 새 규칙 세트를 생성하십시오. 그렇지 않으면 기존 것을 사용할 수 있습니다.
이메일 전달 기능을 처리하는 규칙을 만듭니다.
수신자 구성 페이지에서 이메일을 전달할 이메일 주소를 추가하세요.
작업 구성 페이지에서 먼저 S3 작업을 추가한 다음 Lambda 작업을 추가합니다.
S3 작업의 경우: 기존 S3 버킷을 생성하거나 선택합니다. (선택 사항) 객체 키 접두사를 추가합니다. 메시지 암호화를 선택 취소하고 SNS 주제를 [없음]으로 설정하세요.
Lambda 작업의 경우: SesForwarder Lambda 함수를 선택합니다. 통화 유형을 이벤트로, SNS 주제를 [없음]으로 설정하세요.
마지막으로 규칙의 이름을 지정하고 활성화되어 있는지, 스팸 및 바이러스 검사를 사용하는지 확인하세요.
"버킷에 쓸 수 없습니다"와 같은 오류가 발생하면 이 단계를 완료하기 전에 7단계를 따르세요.
SES가 Lambda:InvokeFunction에 대한 액세스 권한을 추가해 보라고 요청하면 동의하세요.
IAM 사용자가 S3 버킷에 대한 읽기 및 쓰기 액세스 권한을 갖도록 S3 버킷 정책을 구성해야 합니다. SES에서 S3 작업을 설정하면 객체에 대한 루트 액세스를 제외한 모든 사용자를 거부하는 버킷 정책 설명이 추가될 수 있습니다. 이로 인해 Lambda 스크립트에 액세스 문제가 발생할 수 있으므로 다음과 같은 설명을 사용하여 버킷 정책 설명을 조정해야 할 수도 있습니다.
{ "Version": "2012-10-17", "Statement": [ { "Sid": "GiveSESPermissionToWriteEmail", "Effect": "Allow", "Principal": { "Service": "ses.amazonaws.com" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::S3-BUCKET-NAME/*", "Condition": { "StringEquals": { "aws:Referer": "AWS-ACCOUNT-ID" } } } ] }
(선택 사항) 저장된 이메일을 정리하기 위해 며칠 후에 객체를 삭제/만료하도록 이 버킷에 대한 S3 수명 주기를 설정합니다.
이 답변이 생성되었을 때 한두 가지 변경 사항을 적용하여 스크립트 버전을 게시하겠습니다.
확인된 도메인을 통해 두 번 라우팅되는 이메일이 이 스크립트에 의해 변경된 것을 확인하여 미적 측면에서 이를 수정했습니다.
"use strict";
var AWS = require('aws-sdk');
console.log("AWS Lambda SES Forwarder // @arithmetric // Version 4.2.0");
// Configure the S3 bucket and key prefix for stored raw emails, and the
// mapping of email addresses to forward from and to.
//
// Expected keys/values:
//
// - fromEmail: Forwarded emails will come from this verified address
//
// - subjectPrefix: Forwarded emails subject will contain this prefix
//
// - emailBucket: S3 bucket name where SES stores emails.
//
// - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
// trailing slash.
//
// - forwardMapping: Object where the key is the lowercase email address from
// which to forward and the value is an array of email addresses to which to
// send the message.
//
// To match all email addresses on a domain, use a key without the name part
// of an email address before the "at" symbol (i.e. `@example.com`).
//
// To match a mailbox name on all domains, use a key without the "at" symbol
// and domain part of an email address (i.e. `info`).
var defaultConfig = {
fromEmail: "",
subjectPrefix: "",
emailBucket: "ses-sammaye",
emailKeyPrefix: "email/",
forwardMapping: {
"@vvv.com": [
"[email protected]"
],
"@fff.com": [
"[email protected]"
],
"@ggg.com": [
"[email protected]"
],
},
verifiedDomains: [
'vvv.com',
'fff.com',
'ggg.com'
]
};
/**
* Parses the SES event record provided for the `mail` and `receipients` data.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.parseEvent = function(data) {
// Validate characteristics of a SES event record.
if (!data.event ||
!data.event.hasOwnProperty('Records') ||
data.event.Records.length !== 1 ||
!data.event.Records[0].hasOwnProperty('eventSource') ||
data.event.Records[0].eventSource !== 'aws:ses' ||
data.event.Records[0].eventVersion !== '1.0') {
data.log({message: "parseEvent() received invalid SES message:",
level: "error", event: JSON.stringify(data.event)});
return Promise.reject(new Error('Error: Received invalid SES message.'));
}
data.email = data.event.Records[0].ses.mail;
data.recipients = data.event.Records[0].ses.receipt.recipients;
return Promise.resolve(data);
};
/**
* Transforms the original recipients to the desired forwarded destinations.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.transformRecipients = function(data) {
var newRecipients = [];
data.originalRecipients = data.recipients;
data.recipients.forEach(function(origEmail) {
var origEmailKey = origEmail.toLowerCase();
if (data.config.forwardMapping.hasOwnProperty(origEmailKey)) {
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailKey]);
data.originalRecipient = origEmail;
} else {
var origEmailDomain;
var origEmailUser;
var pos = origEmailKey.lastIndexOf("@");
if (pos === -1) {
origEmailUser = origEmailKey;
} else {
origEmailDomain = origEmailKey.slice(pos);
origEmailUser = origEmailKey.slice(0, pos);
}
if (origEmailDomain &&
data.config.forwardMapping.hasOwnProperty(origEmailDomain)) {
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailDomain]);
data.originalRecipient = origEmail;
} else if (origEmailUser &&
data.config.forwardMapping.hasOwnProperty(origEmailUser)) {
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailUser]);
data.originalRecipient = origEmail;
}
}
});
if (!newRecipients.length) {
data.log({message: "Finishing process. No new recipients found for " +
"original destinations: " + data.originalRecipients.join(", "),
level: "info"});
return data.callback();
}
data.recipients = newRecipients;
return Promise.resolve(data);
};
/**
* Fetches the message data from S3.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.fetchMessage = function(data) {
// Copying email object to ensure read permission
data.log({level: "info", message: "Fetching email at s3://" +
data.config.emailBucket + '/' + data.config.emailKeyPrefix +
data.email.messageId});
return new Promise(function(resolve, reject) {
data.s3.copyObject({
Bucket: data.config.emailBucket,
CopySource: data.config.emailBucket + '/' + data.config.emailKeyPrefix +
data.email.messageId,
Key: data.config.emailKeyPrefix + data.email.messageId,
ACL: 'private',
ContentType: 'text/plain',
StorageClass: 'STANDARD'
}, function(err) {
if (err) {
data.log({level: "error", message: "copyObject() returned error:",
error: err, stack: err.stack});
return reject(
new Error("Error: Could not make readable copy of email."));
}
// Load the raw email from S3
data.s3.getObject({
Bucket: data.config.emailBucket,
Key: data.config.emailKeyPrefix + data.email.messageId
}, function(err, result) {
if (err) {
data.log({level: "error", message: "getObject() returned error:",
error: err, stack: err.stack});
return reject(
new Error("Error: Failed to load message body from S3."));
}
data.emailData = result.Body.toString();
return resolve(data);
});
});
});
};
/**
* Processes the message data, making updates to recipients and other headers
* before forwarding message.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.processMessage = function(data) {
var match = data.emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
var header = match && match[1] ? match[1] : data.emailData;
var body = match && match[2] ? match[2] : '';
// Add "Reply-To:" with the "From" address if it doesn't already exists
if (!/^Reply-To: /mi.test(header)) {
match = header.match(/^From: (.*(?:\r?\n\s+.*)*\r?\n)/m);
var from = match && match[1] ? match[1] : '';
if (from) {
header = header + 'Reply-To: ' + from;
data.log({level: "info", message: "Added Reply-To address of: " + from});
} else {
data.log({level: "info", message: "Reply-To address not added because " +
"From address was not properly extracted."});
}
}
// SES does not allow sending messages from an unverified address,
// so replace the message's "From:" header with the original
// recipient (which is a verified domain)
header = header.replace(
/^From: (.*(?:\r?\n\s+.*)*)/mg,
function(match, from) {
var fromText;
var fromEmailDomain = from.replace(/(.*)</, '').replace(/.*@/, "").replace('>', '').trim();
if (data.config.verifiedDomains.indexOf(fromEmailDomain) === -1) {
if (data.config.fromEmail) {
fromText = 'From: ' + from.replace(/<(.*)>/, '').trim() +
' <' + data.config.fromEmail + '>';
} else {
fromText = 'From: ' + from.replace('<', 'at ').replace('>', '') +
' <' + data.originalRecipient + '>';
}
} else {
fromText = 'From: ' + from;
}
return fromText;
});
// Add a prefix to the Subject
if (data.config.subjectPrefix) {
header = header.replace(
/^Subject: (.*)/mg,
function(match, subject) {
return 'Subject: ' + data.config.subjectPrefix + subject;
});
}
// Replace original 'To' header with a manually defined one
if (data.config.toEmail) {
header = header.replace(/^To: (.*)/mg, () => 'To: ' + data.config.toEmail);
}
// Remove the Return-Path header.
header = header.replace(/^Return-Path: (.*)\r?\n/mg, '');
// Remove Sender header.
header = header.replace(/^Sender: (.*)\r?\n/mg, '');
// Remove Message-ID header.
header = header.replace(/^Message-ID: (.*)\r?\n/mig, '');
// Remove all DKIM-Signature headers to prevent triggering an
// "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
// These signatures will likely be invalid anyways, since the From
// header was modified.
header = header.replace(/^DKIM-Signature: .*\r?\n(\s+.*\r?\n)*/mg, '');
data.emailData = header + body;
return Promise.resolve(data);
};
/**
* Send email using the SES sendRawEmail command.
*
* @param {object} data - Data bundle with context, email, etc.
*
* @return {object} - Promise resolved with data.
*/
exports.sendMessage = function(data) {
var params = {
Destinations: data.recipients,
Source: data.originalRecipient,
RawMessage: {
Data: data.emailData
}
};
data.log({level: "info", message: "sendMessage: Sending email via SES. " +
"Original recipients: " + data.originalRecipients.join(", ") +
". Transformed recipients: " + data.recipients.join(", ") + "."});
return new Promise(function(resolve, reject) {
data.ses.sendRawEmail(params, function(err, result) {
if (err) {
data.log({level: "error", message: "sendRawEmail() returned error.",
error: err, stack: err.stack});
return reject(new Error('Error: Email sending failed.'));
}
data.log({level: "info", message: "sendRawEmail() successful.",
result: result});
resolve(data);
});
});
};
/**
* Handler function to be invoked by AWS Lambda with an inbound SES email as
* the event.
*
* @param {object} event - Lambda event from inbound email received by AWS SES.
* @param {object} context - Lambda context object.
* @param {object} callback - Lambda callback object.
* @param {object} overrides - Overrides for the default data, including the
* configuration, SES object, and S3 object.
*/
exports.handler = function(event, context, callback, overrides) {
var steps = overrides && overrides.steps ? overrides.steps :
[
exports.parseEvent,
exports.transformRecipients,
exports.fetchMessage,
exports.processMessage,
exports.sendMessage
];
var data = {
event: event,
callback: callback,
context: context,
config: overrides && overrides.config ? overrides.config : defaultConfig,
log: overrides && overrides.log ? overrides.log : console.log,
ses: overrides && overrides.ses ? overrides.ses : new AWS.SES(),
s3: overrides && overrides.s3 ?
overrides.s3 : new AWS.S3({signatureVersion: 'v4'})
};
Promise.series(steps, data)
.then(function(data) {
data.log({level: "info", message: "Process finished successfully."});
return data.callback();
})
.catch(function(err) {
data.log({level: "error", message: "Step returned error: " + err.message,
error: err, stack: err.stack});
return data.callback(new Error("Error: Step returned error."));
});
};
Promise.series = function(promises, initValue) {
return promises.reduce(function(chain, promise) {
if (typeof promise !== 'function') {
return Promise.reject(new Error("Error: Invalid promise item: " +
promise));
}
return chain.then(promise);
}, Promise.resolve(initValue));
};