mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-10 17:47:07 +08:00
53cda36329
update
279 lines
8.4 KiB
JavaScript
279 lines
8.4 KiB
JavaScript
'use strict';
|
|
|
|
const ObjectId = require('mongodb').ObjectId;
|
|
const GridFSBucket = require('mongodb').GridFSBucket;
|
|
const log = require('npmlog');
|
|
const libmime = require('libmime');
|
|
const { normalizeAddress } = require('./tools');
|
|
const TaskHandler = require('./task-handler');
|
|
|
|
class AuditHandler {
|
|
constructor(options) {
|
|
this.options = options || {};
|
|
|
|
this.database = options.database;
|
|
this.users = options.user || options.database;
|
|
this.gridfs = options.gridfs || options.database;
|
|
|
|
this.loggelf = options.loggelf || (() => false);
|
|
|
|
this.taskHandler = new TaskHandler({ database: this.database });
|
|
|
|
this.bucketName = this.options.bucket || 'audit';
|
|
this.gridstore = new GridFSBucket(this.gridfs, {
|
|
bucketName: this.bucketName,
|
|
chunkSizeBytes: 255 * 1024,
|
|
writeConcern: { w: this.options.writeConcern || 1 }
|
|
});
|
|
}
|
|
|
|
async create(options) {
|
|
options = options || {};
|
|
|
|
if (!options.user || !ObjectId.isValid(options.user)) {
|
|
let err = new Error('Missing user ID');
|
|
err.responseCode = 400;
|
|
err.code = 'InputValidationError';
|
|
throw err;
|
|
}
|
|
|
|
let auditData = {
|
|
user: typeof options.user === 'string' ? new ObjectId(options.user) : options.user,
|
|
start: options.start, // Date or null
|
|
end: options.end, // Date or null
|
|
expires: options.expires, // Date
|
|
deleted: false, // Boolean
|
|
notes: options.notes, // String
|
|
meta: options.meta || {}, // Object
|
|
|
|
import: {
|
|
status: 'queued',
|
|
failed: 0,
|
|
copied: 0
|
|
},
|
|
|
|
audited: 0,
|
|
lastAuditedMessage: null
|
|
};
|
|
|
|
let r;
|
|
try {
|
|
r = await this.database.collection('audits').insertOne(auditData);
|
|
} catch (err) {
|
|
err.responseCode = 500;
|
|
err.code = 'InternalDatabaseError';
|
|
throw err;
|
|
}
|
|
|
|
if (!r.insertedId) {
|
|
let err = new Error('Failed to create audit entry');
|
|
err.responseCode = 500;
|
|
err.code = 'InternalDatabaseError';
|
|
throw err;
|
|
}
|
|
|
|
auditData._id = r.insertedId;
|
|
|
|
try {
|
|
await this.taskHandler.add('audit', {
|
|
audit: auditData._id,
|
|
user: auditData.user,
|
|
start: auditData.start,
|
|
end: auditData.end
|
|
});
|
|
} catch (err) {
|
|
// try to rollback
|
|
err.responseCode = 500;
|
|
err.code = 'InternalDatabaseError';
|
|
|
|
try {
|
|
await this.database.collection('audits').deleteOne({ _id: auditData._id });
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
|
|
return auditData._id;
|
|
}
|
|
|
|
/**
|
|
* Store message to audit GridFS
|
|
*
|
|
* @param {ObjectId} audit ID of the audit session
|
|
* @param {Mixed} message Either a Buffer, an Array of Buffers or a Stream
|
|
* @param {Object} metadata Metadata for the stored message
|
|
*/
|
|
async store(audit, message, metadata, skipCounters) {
|
|
if (!message) {
|
|
throw new Error('Missing message content');
|
|
}
|
|
|
|
if (typeof message === 'string') {
|
|
message = Buffer.from(message);
|
|
}
|
|
|
|
let id = new ObjectId();
|
|
|
|
metadata = metadata || {};
|
|
metadata.audit = metadata.audit || audit;
|
|
metadata.date = metadata.date || new Date();
|
|
|
|
const headers = metadata.header || {};
|
|
|
|
metadata.subject = ([].concat(headers.subject || []).pop() || '').trim();
|
|
try {
|
|
metadata.subject = libmime.decodeWords(metadata.subject);
|
|
} catch (E) {
|
|
// ignore
|
|
}
|
|
|
|
metadata.addresses = [];
|
|
['from', 'to', 'cc', 'bcc'].forEach(type => {
|
|
if (Array.isArray(headers[type])) {
|
|
headers[type].forEach(addr => {
|
|
let entry = {
|
|
name: addr.name,
|
|
address: normalizeAddress(addr.address),
|
|
type
|
|
};
|
|
try {
|
|
entry.name = libmime.decodeWords(entry.name);
|
|
} catch (E) {
|
|
// ignore
|
|
}
|
|
metadata.addresses.push(entry);
|
|
});
|
|
}
|
|
});
|
|
|
|
let result = await new Promise((resolve, reject) => {
|
|
let stream = this.gridstore.openUploadStreamWithId(id, null, {
|
|
contentType: 'message/rfc822',
|
|
metadata
|
|
});
|
|
|
|
stream.once('finish', () => resolve(id));
|
|
|
|
if (Buffer.isBuffer(message)) {
|
|
message = [message];
|
|
}
|
|
|
|
let writeChunks = async () => {
|
|
// write chunk by chunk
|
|
for (let chunk of message) {
|
|
if (stream.write(chunk) === false) {
|
|
await new Promise(resolve => {
|
|
stream.once('drain', resolve);
|
|
});
|
|
}
|
|
}
|
|
stream.end();
|
|
};
|
|
|
|
if (Array.isArray(message)) {
|
|
return writeChunks().catch(err => reject(err));
|
|
}
|
|
|
|
message.on('error', err => {
|
|
stream.emit('error', err);
|
|
});
|
|
|
|
message.pipe(stream);
|
|
});
|
|
|
|
if (result && !skipCounters) {
|
|
await this.database.collection('audits').updateOne({ _id: metadata.audit }, { $inc: { audited: 1 }, $set: { lastAuditedMessage: new Date() } });
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Retrieve message from audit GridFS
|
|
*
|
|
* @param {ObjectId} audit ID of the audit session
|
|
* @param {Mixed} message Either a Buffer, an Array of Buffers or a Stream
|
|
* @param {Object} metadata Metadata for the stored message
|
|
*/
|
|
async retrieve(id) {
|
|
let row = await this.gridfs.collection('audit.files').findOne({ _id: id });
|
|
if (!row) {
|
|
return false;
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
let stream = this.gridstore.openDownloadStream(id);
|
|
if (stream) {
|
|
resolve(stream);
|
|
} else {
|
|
reject(new Error('Failed to open stream'));
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
async updateDeliveryStatus(queueId, seq, status, info) {
|
|
await this.gridfs.collection('audit.files').updateMany(
|
|
{ 'metadata.info.queueId': queueId },
|
|
{
|
|
$push: {
|
|
'metadata.info.delivery': {
|
|
seq,
|
|
status,
|
|
time: new Date(),
|
|
info
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
async removeAudit(auditData) {
|
|
let cursor = await this.gridfs.collection('audit.files').find({
|
|
'metadata.audit': auditData._id
|
|
});
|
|
|
|
let messages = 0;
|
|
let messageData;
|
|
while ((messageData = await cursor.next())) {
|
|
try {
|
|
await this.gridstore.delete(messageData._id);
|
|
messages++;
|
|
} catch (err) {
|
|
log.error('Audit', 'Failed to delete message %s. %s', messageData._id, err.message);
|
|
}
|
|
}
|
|
await cursor.close();
|
|
|
|
await this.database.collection('audits').updateOne({ _id: auditData._id }, { $set: { deleted: true, deletedTime: new Date() } });
|
|
log.info('Audit', 'Deleted audit %s (%s messages)', auditData._id, messages);
|
|
return {
|
|
audit: auditData._id,
|
|
messages
|
|
};
|
|
}
|
|
|
|
async cleanExpired() {
|
|
let expiredAudits = await this.database
|
|
.collection('audits')
|
|
.find({
|
|
expires: { $lt: new Date(), deleted: false }
|
|
})
|
|
.toArray();
|
|
|
|
for (let auditData of expiredAudits) {
|
|
try {
|
|
await this.removeAudit(auditData);
|
|
} catch (err) {
|
|
log.error('Audit', 'Failed to delete expired audit %s. %s', auditData._id, err.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = AuditHandler;
|