Files
copymailrpi/processor/process_mail.php
2026-05-05 15:44:47 +02:00

119 lines
4.3 KiB
PHP

#!/usr/bin/env php
<?php
/**
* process_mail.php — Traitement des emails entrants
* Lancé par cron toutes les minutes.
* Pour chaque compte actif, scanne le Maildir/new/, extrait les pièces jointes
* et les dépose dans /srv/copymail/<username>/
*/
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/../web/includes/db.php';
use ZBateson\MailMimeParser\MailMimeParser;
$parser = new MailMimeParser();
$accounts = db()->query('SELECT * FROM accounts WHERE active = 1')->fetchAll();
foreach ($accounts as $account) {
process_account($account, $parser);
}
function process_account(array $account, MailMimeParser $parser): void {
$domain = cfg('domain');
$maildirNew = "/var/mail/vhosts/{$domain}/{$account['username']}/Maildir/new";
$destDir = $account['smb_path'];
if (!is_dir($maildirNew)) return;
$files = glob($maildirNew . '/*');
if (empty($files)) return;
foreach ($files as $filePath) {
if (!is_file($filePath)) continue;
$fileName = basename($filePath);
// Vérifie si déjà traité
$st = db()->prepare('SELECT id FROM source_file WHERE file_name = ? AND account_id = ?');
$st->execute([$fileName, $account['id']]);
if ($st->fetch()) {
// Déjà traité, déplace quand même
rename($filePath, "/var/mail/vhosts/{$domain}/{$account['username']}/Maildir/cur/{$fileName}:2,S");
continue;
}
$handle = fopen($filePath, 'r');
$message = $parser->parse($handle, true); // true = parser gère la fermeture
$subject = $message->getHeaderValue('Subject') ?? '(sans sujet)';
$from = '';
$fromHdr = $message->getHeader('From');
if ($fromHdr) {
$from = method_exists($fromHdr, 'getEmail') ? $fromHdr->getEmail() : $fromHdr->getRawValue();
}
// Insère l'email source
$st = db()->prepare(
'INSERT INTO source_file (account_id, title, sender, file_name) VALUES (?, ?, ?, ?)'
);
$st->execute([$account['id'], $subject, $from, $fileName]);
$sourceId = (int)db()->lastInsertId();
// Extrait les pièces jointes
$attachments = $message->getAllAttachmentParts();
if (!empty($attachments)) {
if (!is_dir($destDir)) mkdir($destDir, 0750, true);
foreach ($attachments as $att) {
$attName = $att->getHeaderParameter('Content-Disposition', 'filename')
?? $att->getHeaderParameter('Content-Type', 'name')
?? 'attachment_' . time();
// Sécurise le nom de fichier
$attName = preg_replace('/[^a-zA-Z0-9._\- ]/', '_', $attName);
$destPath = $destDir . '/' . $attName;
// Évite les collisions de nom
if (file_exists($destPath)) {
$ext = pathinfo($attName, PATHINFO_EXTENSION);
$base = pathinfo($attName, PATHINFO_FILENAME);
$destPath = $destDir . '/' . $base . '_' . time() . '.' . $ext;
$attName = basename($destPath);
}
$att->saveContent($destPath);
db()->prepare('INSERT INTO files (source_file_id, file_name, path) VALUES (?, ?, ?)')
->execute([$sourceId, $attName, $destPath]);
}
}
// Déplace le mail vers cur/ (marque comme lu)
rename($filePath, "/var/mail/vhosts/{$domain}/{$account['username']}/Maildir/cur/{$fileName}:2,S");
// Webhook
if (!empty($account['webhook_url'])) {
fire_webhook($account['webhook_url'], [
'account' => $account['username'],
'email' => $account['email'],
'subject' => $subject,
'from' => $from,
'source_id' => $sourceId,
'files' => $attachments ? count($attachments) : 0,
'received_at'=> date('c'),
]);
}
}
}
function fire_webhook(string $url, array $payload): void {
$ctx = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($payload),
'timeout' => 5,
'ignore_errors' => true,
]]);
@file_get_contents($url, false, $ctx);
}