120 lines
4.3 KiB
PHP
120 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, false);
|
|
fclose($handle);
|
|
|
|
$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);
|
|
}
|