Initial commit on Forgejo
Some checks are pending
CI and deploy / Test (push) Waiting to run
CI and deploy / Build, push, and deploy (push) Blocked by required conditions

Fresh repository history for elektrine/elektrine-haraka hosted at
https://git.elektrine.com/elektrine/elektrine-haraka.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:42 -04:00
commit 67c1d41a0c
82 changed files with 8713 additions and 0 deletions

181
lib/attachment-handler.js Normal file
View file

@ -0,0 +1,181 @@
/**
* Attachment Handler Module
*
* Extracts and processes email attachments from parsed emails.
* Converts attachment content to base64 for API transmission.
*/
'use strict';
/**
* Extract attachment information from a parsed email
*
* @param {Object} transaction - Haraka transaction object
* @param {Object} parsed - Parsed email from mailparser
* @param {Object} [options] - Options
* @param {boolean} [options.include_content=true] - Include base64 content
* @param {boolean} [options.debug=false] - Enable debug logging
* @param {Function} [options.logger] - Logger function
* @returns {Object} Object with attachments array and count
*/
function extract(transaction, parsed, options = {}) {
const {
include_content = true,
debug = false,
logger = console.log
} = options;
if (debug) logger('Extracting attachment information');
let attachments = [];
let attachment_count = 0;
// Primary: Use mailparser attachments (best source for content)
if (parsed && parsed.attachments && parsed.attachments.length > 0) {
if (debug) logger(`Found ${parsed.attachments.length} parsed attachments`);
attachment_count = parsed.attachments.length;
attachments = parsed.attachments.map((att, index) => {
return format_attachment(att, index, include_content, debug, logger);
});
}
// Fallback: Check transaction notes for attachment plugin results
else if (transaction && transaction.notes && transaction.notes.attachment) {
if (debug) logger('Using attachment notes fallback');
const att_notes = transaction.notes.attachment;
if (att_notes.files && Array.isArray(att_notes.files)) {
attachment_count = att_notes.files.length;
attachments = att_notes.files.map((file, index) => ({
filename: file.filename || file.name || `attachment_${index}`,
content_type: file.ctype || file.content_type || 'application/octet-stream',
size: file.bytes || file.size || 0,
md5: file.md5,
content: null, // No content available from plugin notes
encoding: 'base64',
index: index
}));
}
}
if (debug) {
const with_content = attachments.filter(a => a.content).length;
logger(`Final attachment count: ${attachment_count}, with content: ${with_content}`);
}
return {
attachments,
count: attachment_count,
has_attachments: attachment_count > 0
};
}
/**
* Format a single attachment for API transmission
*
* @param {Object} att - Attachment object from mailparser
* @param {number} index - Attachment index
* @param {boolean} include_content - Include base64 content
* @param {boolean} debug - Debug mode
* @param {Function} logger - Logger function
* @returns {Object} Formatted attachment object
*/
function format_attachment(att, index, include_content, debug, logger) {
let content_base64 = null;
if (include_content && att.content) {
content_base64 = to_base64(att.content);
}
const attachment_info = {
filename: att.filename || att.name || `attachment_${index}`,
content_type: att.contentType || att.content_type || 'application/octet-stream',
size: att.size || (att.content ? att.content.length : 0),
content_id: att.cid || null,
content: content_base64,
encoding: 'base64',
index: index
};
if (debug) {
logger(`Attachment ${index}: ${attachment_info.filename}, ` +
`${attachment_info.size} bytes, type: ${attachment_info.content_type}`);
}
return attachment_info;
}
/**
* Convert content to base64 string
*
* @param {Buffer|string} content - Content to convert
* @returns {string|null} Base64 encoded string or null
*/
function to_base64(content) {
if (!content) return null;
if (Buffer.isBuffer(content)) {
return content.toString('base64');
}
if (typeof content === 'string') {
return Buffer.from(content).toString('base64');
}
return null;
}
/**
* Calculate total attachment size
*
* @param {Array} attachments - Array of attachment objects
* @returns {number} Total size in bytes
*/
function get_total_size(attachments) {
if (!attachments || !Array.isArray(attachments)) return 0;
return attachments.reduce((total, att) => {
return total + (att.size || 0);
}, 0);
}
/**
* Check if any attachment exceeds size limit
*
* @param {Array} attachments - Array of attachment objects
* @param {number} max_size - Maximum size in bytes
* @returns {boolean} True if any attachment exceeds limit
*/
function has_oversized(attachments, max_size) {
if (!attachments || !Array.isArray(attachments)) return false;
return attachments.some(att => (att.size || 0) > max_size);
}
/**
* Filter attachments by content type
*
* @param {Array} attachments - Array of attachment objects
* @param {string|string[]} content_types - Content type(s) to match
* @returns {Array} Filtered attachments
*/
function filter_by_type(attachments, content_types) {
if (!attachments || !Array.isArray(attachments)) return [];
const types = Array.isArray(content_types) ? content_types : [content_types];
return attachments.filter(att => {
const att_type = (att.content_type || '').toLowerCase();
return types.some(type => att_type.includes(type.toLowerCase()));
});
}
module.exports = {
extract,
format_attachment,
to_base64,
get_total_size,
has_oversized,
filter_by_type
};