Files
docker-compose-hatthieves/production/haraka-wildduck/zonemta-wildduck.js
2020-08-02 23:24:58 +00:00

1002 líneas
38 KiB
JavaScript

'use strict';
const os = require('os');
const addressparser = require('nodemailer/lib/addressparser');
const MimeNode = require('nodemailer/lib/mime-node');
const MessageHandler = require('wildduck/lib/message-handler');
const UserHandler = require('wildduck/lib/user-handler');
const DkimHandler = require('wildduck/lib/dkim-handler');
const AuditHandler = require('wildduck/lib/audit-handler');
const wdErrors = require('wildduck/lib/errors');
const counters = require('wildduck/lib/counters');
const tools = require('wildduck/lib/tools');
const SRS = require('srs.js');
const Gelf = require('gelf');
const util = require('util');
module.exports.title = 'WildDuck MSA';
module.exports.init = function (app, done) {
const users = new WeakMap();
const redisClient = app.db.redis;
const database = app.db.database;
const usersdb = app.db.users;
const gridfsdb = app.db.gridfs;
const component = ((app.config.gelf && app.config.gelf.component) || 'mta').toUpperCase();
const hostname = app.config.hostname || os.hostname();
const gelf =
app.config.gelf && app.config.gelf.enabled
? new Gelf(app.config.gelf.options)
: {
// placeholder
emit: () => false,
};
wdErrors.setGelf(gelf);
const loggelf = (message) => {
if (!app.config.gelf || !app.config.gelf.enabled) {
return false;
}
if (typeof message === 'string') {
message = {
short_message: message,
};
}
message = message || {};
if (!message.short_message || message.short_message.indexOf(component.toUpperCase()) !== 0) {
message.short_message = component.toUpperCase() + ' ' + (message.short_message || '');
}
message.facility = app.config.gelf.component || 'mta'; // facility is deprecated but set by the driver if not provided
message.host = hostname;
message.timestamp = Date.now() / 1000;
message._component = app.config.gelf.component || 'mta';
Object.keys(message).forEach((key) => {
if (!message[key]) {
delete message[key];
}
});
gelf.emit('gelf.log', message);
};
const dkimHandler = new DkimHandler({
cipher: app.config.dkim && app.config.dkim.cipher,
secret: app.config.dkim && app.config.dkim.secret,
database: app.db.database,
loggelf: (message) => loggelf(message),
});
const ttlcounter = counters(redisClient).ttlcounter;
const srsRewriter = new SRS({
secret: (app.config.srs && app.config.srs.secret) || '?',
});
const messageHandler = new MessageHandler({
database,
redis: redisClient,
users: usersdb,
gridfs: gridfsdb,
attachments: app.config.attachments || {
type: 'gridstore',
bucket: 'attachments',
},
loggelf: (message) => loggelf(message),
});
const userHandler = new UserHandler({
database,
redis: redisClient,
gridfs: gridfsdb,
users: usersdb,
authlogExpireDays: app.config.authlogExpireDays,
loggelf: (message) => loggelf(message),
});
const auditHandler = new AuditHandler({
database,
gridfs: gridfsdb,
users: usersdb,
bucket: 'audit',
loggelf: (message) => loggelf(message),
});
const encryptMessage = util.promisify(messageHandler.encryptMessage.bind(messageHandler));
const prepareMessage = util.promisify(messageHandler.prepareMessage.bind(messageHandler));
const addMessage = util.promisify((...args) => {
let callback = args.pop();
messageHandler.add(...args, (err, status, data) => {
if (err) {
return callback(err);
}
return callback(null, { status, data });
});
});
const interfaces = [].concat(app.config.interfaces || '*');
const allInterfaces = interfaces.includes('*');
// handle user authentication
app.addHook('smtp:auth', (auth, session, next) => {
if (!checkInterface(session.interface)) {
return next();
}
if (auth.method === 'XCLIENT') {
// special proxied connection where authentication is handled upstream
// XCLIENT is only available if smtp server has useXClient option set to true
return userHandler.get(auth.username, { username: true, address: true }, (err, userData) => {
if (err) {
return next(err);
}
if (!userData) {
let message = 'Authentication failed';
err = new Error(message);
err.responseCode = 535;
err.name = 'SMTPResponse'; // do not throw
loggelf({
short_message: '[AUTH FAIL:' + auth.username + '] ' + session.id,
_auth_fail: 'yes',
_mail_action: 'auth',
_username: auth.username,
_xclient: 'yes',
_session_id: session.id,
_ip: session.remoteAddress,
});
return next(err);
}
auth.username = userData.username + '[' + auth.username + ']';
next();
});
}
auth.username = auth.username.split('[').shift()
userHandler.authenticate(
auth.username,
auth.password,
'smtp',
{
protocol: 'SMTP',
ip: session.remoteAddress,
},
(err, result) => {
if (err) {
return next(err);
}
if (!result || (result.scope === 'master' && result.require2fa)) {
let message = 'Authentication failed';
if (result) {
message = 'You need to use an application specific password';
}
err = new Error(message);
err.responseCode = 535;
err.name = 'SMTPResponse'; // do not throw
loggelf({
short_message: '[AUTH FAIL:' + auth.username + '] ' + session.id,
_auth_fail: 'yes',
_mail_action: 'auth',
_username: auth.username,
_require_asp: result ? 'yes' : '',
_session_id: session.id,
_ip: session.remoteAddress,
});
return next(err);
}
loggelf({
short_message: '[AUTH OK:' + auth.username + '] ' + session.id,
_auth_ok: 'yes',
_mail_action: 'auth',
_username: auth.username,
_scope: result.scope,
_session_id: session.id,
_ip: session.remoteAddress,
});
auth.username = result.username + '[' + auth.username + ']';
next();
}
);
});
// Check if an user is allowed to use specific address, if not then override using the default
app.addHook('message:headers', (envelope, messageInfo, next) => {
if (!checkInterface(envelope.interface)) {
return next();
}
// Check From: value. Add if missing or rewrite if needed
let headerFrom = envelope.headers.getFirst('from');
let headerFromList;
let headerFromObj;
if (headerFrom) {
headerFromList = addressparser(headerFrom);
if (headerFromList.length) {
headerFromObj = headerFromList[0] || {};
if (headerFromObj.group) {
headerFromObj = {};
}
}
}
getUser(envelope, (err, userData) => {
if (err) {
return next(err);
}
let normalizedAddress;
normalizedAddress = tools.normalizeAddress(envelope.from);
normalizedAddress =
normalizedAddress.substr(0, normalizedAddress.indexOf('@')).replace(/\./g, '') + normalizedAddress.substr(normalizedAddress.indexOf('@'));
let checkAddress = (address, done) => {
if (userData.fromWhitelist && userData.fromWhitelist.length) {
if (
userData.fromWhitelist.some((addr) => {
if (addr === address) {
return true;
}
if (addr.charAt(0) === '*' && address.indexOf(addr.substr(1)) >= 0) {
return true;
}
if (addr.charAt(addr.length - 1) === '*' && address.indexOf(addr.substr(0, addr.length - 1)) === 0) {
return true;
}
return false;
})
) {
// generate address object for whitelisted address
let normalizedAddress = tools.normalizeAddress(address);
normalizedAddress =
normalizedAddress.substr(0, normalizedAddress.indexOf('@')).replace(/\./g, '') +
normalizedAddress.substr(normalizedAddress.indexOf('@'));
return done(null, {
address,
addrview: normalizedAddress,
});
}
}
userHandler.resolveAddress(address, { wildcard: true }, (err, addressData) => {
if (err) {
return done(err);
}
if (!addressData) {
return done(null, false);
}
if (addressData.user) {
if (addressData.user.toString() === userData._id.toString()) {
return done(null, addressData);
} else {
return done(null, false);
}
}
if (addressData.targets) {
if (addressData.targets.find((target) => target.user && target.user.toString() === userData._id.toString())) {
return done(null, addressData);
} else {
return done(null, false);
}
}
return done(null, false);
});
};
checkAddress(envelope.from, (err, addressData) => {
if (err) {
return next(err);
}
if (!addressData) {
loggelf({
short_message: '[RWENVELOPE] ' + envelope.id,
_mail_action: 'rw_envelope_from',
_queue_id: envelope.id,
_envelope_from: envelope.from,
_rewrite_from: userData.address,
});
// replace MAIL FROM address
app.logger.info(
'Rewrite',
'%s RWENVELOPE User %s tries to use "%s" as Return Path address, replacing with "%s"',
envelope.id,
userData.username,
envelope.from + (envelope.from === normalizedAddress ? '' : '[' + normalizedAddress + ']'),
userData.address
);
envelope.from = userData.address;
}
if (!headerFromObj) {
return next();
}
normalizedAddress = tools.normalizeAddress(Buffer.from(headerFromObj.address, 'binary').toString());
normalizedAddress =
normalizedAddress.substr(0, normalizedAddress.indexOf('@')).replace(/\./g, '') + normalizedAddress.substr(normalizedAddress.indexOf('@'));
if (addressData && addressData.addrview === normalizedAddress) {
// same address
return next();
}
checkAddress(Buffer.from(headerFromObj.address, 'binary').toString(), (err, addressData) => {
if (err) {
return next(err);
}
if (addressData) {
// can send mail as this user
return next();
}
loggelf({
short_message: '[RWFROM] ' + envelope.id,
_mail_action: 'rw_header_from',
_queue_id: envelope.id,
_header_from: headerFromObj.address,
_rewrite_from: envelope.from,
});
app.logger.info(
'Rewrite',
'%s RWFROM User %s tries to use "%s" as From address, replacing with "%s"',
envelope.id,
userData.username,
headerFromObj.address + (headerFromObj.address === normalizedAddress ? '' : '[' + normalizedAddress + ']'),
envelope.from
);
headerFromObj.address = envelope.from;
let rootNode = new MimeNode();
let newHeaderFrom = rootNode._convertAddresses([headerFromObj]);
envelope.headers.update('From', newHeaderFrom);
envelope.headers.update('X-WildDuck-Original-From', headerFrom);
next();
});
});
});
});
// Check if the user can send to yet another recipient
app.addHook('smtp:rcpt_to', (address, session, next) => {
if (!checkInterface(session.interface)) {
return next();
}
getUser(session, (err, userData) => {
if (err) {
return next(err);
}
if (!userData.recipients) {
return next();
}
ttlcounter('wdr:' + userData._id.toString(), 1, userData.recipients, false, (err, result) => {
if (err) {
return next(err);
}
let success = result.success;
let sent = result.value;
let ttl = result.ttl;
let ttlHuman = false;
if (ttl) {
if (ttl < 60) {
ttlHuman = ttl + ' seconds';
} else if (ttl < 3600) {
ttlHuman = Math.round(ttl / 60) + ' minutes';
} else {
ttlHuman = Math.round(ttl / 3600) + ' hours';
}
}
if (!success) {
loggelf({
short_message: '[RCPT TO:' + address.address + '] ' + session.id,
_to: address.address,
_mail_action: 'rcpt_to',
_daily: 'yes',
_rate_limit: 'yes',
_error: 'daily sending limit reached',
});
app.logger.info(
'Sender',
'%s RCPTDENY denied %s sent=%s allowed=%s expires=%ss.',
session.envelopeId,
address.address,
sent,
userData.recipients,
ttl
);
let err = new Error('You reached a daily sending limit for your account' + (ttl ? '. Limit expires in ' + ttlHuman : ''));
err.responseCode = 550;
err.name = 'SMTPResponse';
return setImmediate(() => next(err));
}
loggelf({
short_message: '[RCPT TO:' + address.address + '] ' + session.id,
_user: userData._id.toString(),
_from: session.envelope.mailFrom && session.envelope.mailFrom.address,
_to: address.address,
_mail_action: 'rcpt_to',
_allowed: 'yes',
});
app.logger.info('Sender', '%s RCPTACCEPT accepted %s sent=%s allowed=%s', session.envelopeId, address.address, sent, userData.recipients);
next();
});
});
});
// Check if an user is allowed to use specific address, if not then override using the default
app.addHook('message:queue', (envelope, messageInfo, next) => {
if (!checkInterface(envelope.interface)) {
return next();
}
getUser(envelope, (err, userData) => {
if (err) {
return next(err);
}
database
.collection('audits')
.find({ user: userData._id })
.toArray((err, audits) => {
if (err) {
// ignore
audits = [];
}
let now = new Date();
audits = audits.filter((auditData) => {
if (auditData.start && auditData.start > now) {
return false;
}
if (auditData.end && auditData.end < now) {
return false;
}
return true;
});
let overQuota = userData.quota && userData.storageUsed > userData.quota;
let addToSent = userData.uploadSentMessages && !overQuota && !app.config.disableUploads;
if (overQuota) {
// not enough storage
app.logger.info('Rewrite', '%s MSAUPLSKIP user=%s message=over quota', envelope.id, envelope.user);
if (!audits.length) {
return next();
}
}
if (!addToSent && !audits.length) {
// nothing to do here
return next();
}
let chunks = [
Buffer.from('Return-Path: ' + envelope.from + '\r\n' + generateReceivedHeader(envelope, hostname) + '\r\n'),
envelope.headers.build(),
];
let chunklen = chunks[0].length + chunks[1].length;
let body = app.manager.queue.gridstore.openDownloadStreamByName('message ' + envelope.id);
body.on('readable', () => {
let chunk;
while ((chunk = body.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
body.once('error', (err) => next(err));
body.once('end', () => {
// Next we try to upload the message to Sent Mail folder
// It doesn't really matter if it succeeds or not so we are not waiting until it's done
setImmediate(next);
// from now on use `return;` to end sequence as next() is already called
let raw = Buffer.concat(chunks, chunklen);
let storeSentMessage = async () => {
// Checks if the message needs to be encrypted before storing it
let messageSource = raw;
if (userData.encryptMessages && userData.pubKey) {
try {
let encrypted = await encryptMessage(userData.pubKey, raw);
if (encrypted) {
messageSource = encrypted;
}
} catch (err) {
// ignore
}
}
try {
let { data } = await addMessage({
user: userData._id,
specialUse: '\\Sent',
outbound: envelope.id,
meta: {
source: 'SMTP',
queueId: envelope.id,
from: envelope.from,
to: envelope.to,
origin: envelope.origin,
originhost: envelope.originhost,
transhost: envelope.transhost,
transtype: envelope.transtype,
time: new Date(),
},
date: false,
flags: ['\\Seen'],
raw: messageSource,
// if similar message exists, then skip
skipExisting: true,
});
if (data) {
app.logger.info('Rewrite', '%s MSAUPLSUCC user=%s uid=%s', envelope.id, envelope.user, data.uid);
} else {
app.logger.info('Rewrite', '%s MSAUPLSKIP user=%s message=already exists', envelope.id, envelope.user);
}
} catch (err) {
app.logger.error('Rewrite', '%s MSAUPLFAIL user=%s error=%s', envelope.id, envelope.user, err.message);
}
};
let processAudits = async () => {
const messageData = await prepareMessage({
raw,
});
if (messageData.attachments && messageData.attachments.length) {
messageData.ha = messageData.attachments.some((a) => !a.related);
} else {
messageData.ha = false;
}
for (let auditData of audits) {
const auditMessage = await auditHandler.store(auditData._id, raw, {
date: now,
msgid: messageData.msgid,
header: messageData.mimeTree && messageData.mimeTree.parsedHeader,
ha: messageData.ha,
info: {
source: 'SMTP',
queueId: envelope.id,
from: envelope.from,
to: envelope.to,
origin: envelope.origin,
originhost: envelope.originhost,
transhost: envelope.transhost,
transtype: envelope.transtype,
time: new Date(),
},
});
app.logger.verbose(
'Rewrite',
'%s AUDITUPL user=%s coll=%s message=%s msgid=%s dst=%s',
envelope.id,
envelope.user,
'Stored message to audit base',
messageData.msgid,
auditMessage
);
}
};
if (addToSent) {
// addMessage also calls audit methods
storeSentMessage().catch((err) =>
app.logger.error('Rewrite', '%s MSAUPLFAIL user=%s error=%s', envelope.id, envelope.user, err.message)
);
} else {
processAudits().catch((err) =>
app.logger.error('Rewrite', '%s MSAUPLFAIL user=%s error=%s', envelope.id, envelope.user, err.message)
);
}
});
});
});
});
// rewrite MAIL FROM: for messages forwarded by user filter
app.addHook('sender:headers', (delivery, connection, next) => {
// Forwarded header if present
if (delivery.forwardedFor) {
delivery.headers.addFormatted('X-Forwarded-For', delivery.forwardedFor, 0);
}
if (!app.config.srs || !app.config.srs.enabled || !delivery.envelope.from || delivery.interface !== 'forwarder' || delivery.skipSRS) {
return next();
}
let from = delivery.envelope.from || '';
let fromDomain = from.substr(from.lastIndexOf('@') + 1).toLowerCase();
let srsDomain = app.config.srs && app.config.srs.rewriteDomain;
try {
delivery.envelope.from = srsRewriter.rewrite(from.substr(0, from.lastIndexOf('@')), fromDomain) + '@' + srsDomain;
delivery.headers.add('X-Original-Sender', from, Infinity);
} catch (E) {
// failed rewriting address, keep as is
app.logger.error('SRS', '%s.%s SRSFAIL Failed rewriting "%s". %s', delivery.id, delivery.seq, from, E.message);
}
delivery.headers.add('X-Zone-Forwarded-For', from, Infinity);
delivery.headers.add('X-Zone-Forwarded-To', delivery.envelope.to, Infinity);
next();
});
app.addHook('sender:connect', (delivery, options, next) => {
if (!delivery.dkim.keys) {
delivery.dkim.keys = [];
}
let from = (delivery.envelope.from || (delivery.parsedEnvelope && delivery.parsedEnvelope.from) || '').toString();
let fromDomain = from.substr(from.lastIndexOf('@') + 1);
let getKey = (domain, done) => {
dkimHandler.get({ domain }, true, (err, keyData) => {
if (err && err.code !== 'DkimNotFound') {
return done(err);
}
if (keyData) {
return done(null, keyData);
}
dkimHandler.get({ domain: '*' }, true, (err, keyData) => {
if (err) {
return done(err);
}
if (keyData) {
return done(null, keyData);
}
return done();
});
});
};
getKey(fromDomain, (err, keyData) => {
if (err && err.code !== 'DkimNotFound') {
app.logger.error('DKIM', '%s.%s DBFAIL Failed loading DKIM key "%s". %s', delivery.id, delivery.seq, fromDomain, err.message);
return next();
}
if (keyData) {
delivery.dkim.keys.push({
domainName: tools.normalizeDomain(fromDomain),
keySelector: keyData.selector,
privateKey: keyData.privateKey,
});
}
if (!app.config.signTransportDomain || delivery.dkim.keys.find((key) => key.domainName === delivery.zoneAddress.name)) {
return next();
}
getKey(delivery.zoneAddress.name, (err, keyData) => {
if (!err && keyData) {
delivery.dkim.keys.push({
domainName: tools.normalizeDomain(delivery.zoneAddress.name),
keySelector: keyData.selector,
privateKey: keyData.privateKey,
});
}
return next();
});
});
});
app.addHook('log:entry', (entry, next) => {
entry.created = new Date();
let message = {
_queue_id: (entry.id || '').toString(),
_queue_id_seq: (entry.seq || '').toString(),
};
let updateAudited = (status, info) => {
auditHandler
.updateDeliveryStatus(entry.id, entry.seq, status, info)
.catch((err) => app.logger.error('Rewrite', '%s.%s LOGERR %s', entry.id, entry.seq, err.message));
};
switch (entry.action) {
case 'QUEUED':
{
let username = (entry.user || entry.auth || '').toString();
let match = username.match(/\[([^\]]+)]/);
if (match && match[1]) {
username = match[1];
}
message.short_message = '[QUEUED] ' + entry.id;
message._from = (entry.from || '').toString();
message._to = (entry.to || '').toString();
message._mail_action = 'queued';
message._message_id = (entry['message-id'] || '').toString().replace(/^[\s<]+|[\s>]+$/g, '');
message._ip = entry.src;
message._body_size = entry.body;
message._spam_score = Number(entry.score) || '';
message._interface = entry.interface;
message._proto = entry.transtype;
message._subject = entry.subject;
message._header_from = entry.headerFrom;
message._authenticated_sender = username;
}
break;
case 'ACCEPTED':
message.short_message = '[ACCEPTED] ' + entry.id + '.' + entry.seq;
message._from = (entry.from || '').toString();
message._to = (entry.to || '').toString();
message._mail_action = 'accepted';
message._zone = entry.zone;
message._mx = entry.mx;
message._mx_host = entry.host;
message._local_ip = entry.ip;
message._response = entry.response;
updateAudited('accepted', {
to: (entry.to || '').toString(),
response: entry.response,
mx: entry.mx,
local_ip: entry.ip,
});
break;
case 'DEFERRED':
message.short_message = '[DEFERRED] ' + entry.id + '.' + entry.seq;
message._from = (entry.from || '').toString();
message._to = (entry.to || '').toString();
message._bounce_category = entry.category;
message._bounce_count = entry.defcount;
message._mail_action = 'deferred';
message._zone = entry.zone;
message._mx = entry.mx;
message._mx_host = entry.host;
message._local_ip = entry.ip;
message._response = entry.response;
updateAudited('deferred', {
to: (entry.to || '').toString(),
response: entry.response,
mx: entry.mx,
local_ip: entry.ip,
});
break;
case 'REJECTED':
message.short_message = '[REJECTED] ' + entry.id + '.' + entry.seq;
message._from = (entry.from || '').toString();
message._to = (entry.to || '').toString();
message._bounce_category = entry.category;
message._bounce_count = entry.defcount;
message._mail_action = 'bounced';
message._zone = entry.zone;
message._mx = entry.mx;
message._mx_host = entry.host;
message._local_ip = entry.ip;
message._response = entry.response;
updateAudited('rejected', {
to: (entry.to || '').toString(),
response: entry.response,
mx: entry.mx,
local_ip: entry.ip,
});
break;
case 'NOQUEUE':
message.short_message = '[NOQUEUE] ' + entry.id + '.' + entry.seq;
message._from = (entry.from || '').toString();
message._to = (entry.to || '').toString();
message._mail_action = 'dropped';
message._message_id = (entry['message-id'] || '').toString().replace(/^[\s<]+|[\s>]+$/g, '');
message._ip = entry.src;
message._body_size = entry.body;
message._spam_score = Number(entry.score) || '';
message._interface = entry.interface;
message._proto = entry.transtype;
message._response = entry.responseText;
break;
case 'DELETED':
message.short_message = '[DELETED] ' + entry.id + '.' + entry.seq;
message._from = (entry.from || '').toString();
message._to = (entry.to || '').toString();
message._mail_action = 'dropped';
message._response = entry.reason;
break;
case 'DROP':
message.short_message = '[DROP] ' + entry.id + '.' + entry.seq;
message._from = (entry.from || '').toString();
message._to = (entry.to || '').toString();
message._mail_action = 'dropped';
message._response = entry.reason;
break;
}
if (message.short_message) {
loggelf(message);
}
return next();
});
function checkInterface(iface) {
if (allInterfaces || interfaces.includes(iface)) {
return true;
}
return false;
}
function getUser(envelope, callback) {
let query = false;
if (users.has(envelope)) {
// user data is already cached
return callback(null, users.get(envelope));
}
if (envelope.user) {
query = {
username: envelope.user.split('[').shift(),
};
}
if (!query) {
let err = new Error('Insufficient user info');
err.responseCode = 550;
err.name = 'SMTPResponse'; // do not throw
return callback(err);
}
usersdb.collection('users').findOne(
query,
{
projection: {
username: true,
address: true,
quota: true,
storageUsed: true,
recipients: true,
encryptMessages: true,
pubKey: true,
uploadSentMessages: true,
disabled: true,
suspended: true,
fromWhitelist: true,
},
},
(err, user) => {
if (err) {
return callback(err);
}
if (!user) {
let err = new Error('User "' + query.username + '" was not found');
err.responseCode = 550;
err.name = 'SMTPResponse'; // do not throw
return callback(err);
}
if (user.disabled || user.suspended) {
let err = new Error('User "' + query.username + '" is currently disabled');
err.responseCode = 550;
err.name = 'SMTPResponse'; // do not throw
return callback(err);
}
users.set(envelope, user);
return callback(null, user);
}
);
}
done();
};
function generateReceivedHeader(envelope, hostname) {
let key = 'Received';
let origin = envelope.origin ? '[' + envelope.origin + ']' : '';
let originhost = envelope.originhost && envelope.originhost.charAt(0) !== '[' ? envelope.originhost : false;
origin = [].concat(origin || []).concat(originhost || []);
if (origin.length > 1) {
origin = '(' + origin.join(' ') + ')';
} else {
origin = origin.join(' ').trim() || 'localhost';
}
let username = (envelope.user || '').toString();
let match = username.match(/\[([^\]]+)]/);
if (match && match[1]) {
username = match[1];
}
let value =
'' +
// from ehlokeyword
'from' +
(envelope.transhost ? ' ' + envelope.transhost : '') +
// [1.2.3.4]
' ' +
origin +
(originhost ? '\r\n' : '') +
// (Authenticated sender: username)
(envelope.user ? ' (Authenticated sender: ' + username + ')\r\n' : !originhost ? '\r\n' : '') +
// by smtphost
' by ' +
hostname +
// with ESMTP
' with ' +
envelope.transtype +
// id 12345678
' id ' +
envelope.id +
// for <receiver@example.com>
(envelope.to.length === 1 ? '\r\n for <' + envelope.to[0] + '>' : '') +
// (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256)
(envelope.tls ? '\r\n (version=' + envelope.tls.version + ' cipher=' + envelope.tls.name + ')' : '') +
';' +
'\r\n' +
// Wed, 03 Aug 2016 11:32:07 +0000
' ' +
new Date(envelope.time).toUTCString().replace(/GMT/, '+0000');
return key + ': ' + value;
}