224 lines
6.9 KiB
PHP
224 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries\Radar;
|
|
|
|
/**
|
|
* Lecture Gmail (readonly) pour une mailbox impersonnée.
|
|
*/
|
|
class GmailReader
|
|
{
|
|
public function __construct(
|
|
private GoogleWorkspaceToken $tokenClient,
|
|
private string $scopes,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @return list<array{
|
|
* gmail_thread_id:string,
|
|
* mailbox:string,
|
|
* subject:string,
|
|
* from_email:string,
|
|
* from_name:string,
|
|
* snippet:string,
|
|
* gmail_url:string,
|
|
* received_at:?string
|
|
* }>
|
|
*/
|
|
public function listRecentThreads(string $mailbox, int $lookbackDays, int $maxMessages): array
|
|
{
|
|
$access = $this->tokenClient->getAccessToken($mailbox, $this->scopes);
|
|
$q = 'newer_than:' . max(1, $lookbackDays) . 'd';
|
|
$url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages?'
|
|
. \http_build_query([
|
|
'q' => $q,
|
|
'maxResults' => $maxMessages,
|
|
]);
|
|
|
|
[$code, $body] = $this->tokenClient->http(
|
|
'GET',
|
|
$url,
|
|
['Authorization: Bearer ' . $access]
|
|
);
|
|
if ($code !== 200) {
|
|
throw new \RuntimeException("Gmail messages.list ({$code}): {$body}");
|
|
}
|
|
|
|
$data = \json_decode($body, true) ?: [];
|
|
$ids = $data['messages'] ?? [];
|
|
if ($ids === []) {
|
|
return [];
|
|
}
|
|
|
|
$byThread = [];
|
|
foreach ($ids as $m) {
|
|
$msgId = $m['id'] ?? null;
|
|
if (! $msgId) {
|
|
continue;
|
|
}
|
|
$detail = $this->getMessageMeta($access, $msgId);
|
|
if ($detail === null) {
|
|
continue;
|
|
}
|
|
$tid = $detail['gmail_thread_id'];
|
|
// Garder le message le plus récent du fil dans ce batch
|
|
if (! isset($byThread[$tid]) || ($detail['internal_ts'] ?? 0) > ($byThread[$tid]['internal_ts'] ?? 0)) {
|
|
$detail['mailbox'] = $mailbox;
|
|
$byThread[$tid] = $detail;
|
|
}
|
|
}
|
|
|
|
$out = array_values($byThread);
|
|
usort($out, static fn ($a, $b) => ($b['internal_ts'] ?? 0) <=> ($a['internal_ts'] ?? 0));
|
|
|
|
foreach ($out as &$row) {
|
|
unset($row['internal_ts']);
|
|
}
|
|
unset($row);
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @return ?array<string, mixed>
|
|
*/
|
|
private function getMessageMeta(string $access, string $messageId): ?array
|
|
{
|
|
$url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/' . \rawurlencode($messageId)
|
|
. '?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date';
|
|
|
|
[$code, $body] = $this->tokenClient->http(
|
|
'GET',
|
|
$url,
|
|
['Authorization: Bearer ' . $access]
|
|
);
|
|
if ($code !== 200) {
|
|
return null;
|
|
}
|
|
|
|
$msg = \json_decode($body, true);
|
|
if (! \is_array($msg)) {
|
|
return null;
|
|
}
|
|
|
|
$headers = [];
|
|
foreach ($msg['payload']['headers'] ?? [] as $h) {
|
|
$headers[\strtolower($h['name'] ?? '')] = $h['value'] ?? '';
|
|
}
|
|
|
|
$from = $this->parseFrom($headers['from'] ?? '');
|
|
$ts = isset($msg['internalDate']) ? (int) \floor(((int) $msg['internalDate']) / 1000) : 0;
|
|
|
|
return [
|
|
'gmail_message_id' => $messageId,
|
|
'gmail_thread_id' => (string) ($msg['threadId'] ?? $messageId),
|
|
'subject' => self::decodeText((string) ($headers['subject'] ?? '(sans sujet)')),
|
|
'from_email' => $from['email'],
|
|
'from_name' => self::decodeText($from['name']),
|
|
'snippet' => self::decodeText((string) ($msg['snippet'] ?? '')),
|
|
'gmail_url' => 'https://mail.google.com/mail/u/0/#all/' . \rawurlencode((string) ($msg['threadId'] ?? $messageId)),
|
|
'received_at' => $ts > 0 ? \date('Y-m-d H:i:s', $ts) : null,
|
|
'internal_ts' => $ts,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Corps texte du message (pour analyse IA). format=full.
|
|
*/
|
|
public function getPlainBody(string $mailbox, string $messageId): string
|
|
{
|
|
$access = $this->tokenClient->getAccessToken($mailbox, $this->scopes);
|
|
$url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/' . \rawurlencode($messageId)
|
|
. '?format=full';
|
|
|
|
[$code, $body] = $this->tokenClient->http(
|
|
'GET',
|
|
$url,
|
|
['Authorization: Bearer ' . $access]
|
|
);
|
|
if ($code !== 200) {
|
|
throw new \RuntimeException("Gmail messages.get ({$code}): {$body}");
|
|
}
|
|
|
|
$msg = \json_decode($body, true);
|
|
if (! \is_array($msg)) {
|
|
return '';
|
|
}
|
|
|
|
$text = $this->extractPlainFromPayload($msg['payload'] ?? []);
|
|
if ($text === '' && ! empty($msg['snippet'])) {
|
|
$text = (string) $msg['snippet'];
|
|
}
|
|
|
|
return self::decodeText(\mb_substr($text, 0, 12000));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function extractPlainFromPayload(array $payload): string
|
|
{
|
|
$mime = \strtolower((string) ($payload['mimeType'] ?? ''));
|
|
if ($mime === 'text/plain' && ! empty($payload['body']['data'])) {
|
|
return $this->b64urlDecode((string) $payload['body']['data']);
|
|
}
|
|
|
|
foreach ($payload['parts'] ?? [] as $part) {
|
|
if (! \is_array($part)) {
|
|
continue;
|
|
}
|
|
$found = $this->extractPlainFromPayload($part);
|
|
if ($found !== '') {
|
|
return $found;
|
|
}
|
|
}
|
|
|
|
if ($mime === 'text/html' && ! empty($payload['body']['data'])) {
|
|
$html = $this->b64urlDecode((string) $payload['body']['data']);
|
|
|
|
return \trim(\html_entity_decode(\strip_tags($html), \ENT_QUOTES | \ENT_HTML5, 'UTF-8'));
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function b64urlDecode(string $data): string
|
|
{
|
|
$raw = \strtr($data, '-_', '+/');
|
|
$pad = \strlen($raw) % 4;
|
|
if ($pad > 0) {
|
|
$raw .= \str_repeat('=', 4 - $pad);
|
|
}
|
|
$out = \base64_decode($raw, true);
|
|
|
|
return $out === false ? '' : $out;
|
|
}
|
|
|
|
/** Gmail renvoie souvent des entités HTML dans snippet / headers. */
|
|
public static function decodeText(string $text): string
|
|
{
|
|
if ($text === '') {
|
|
return '';
|
|
}
|
|
|
|
return \html_entity_decode($text, \ENT_QUOTES | \ENT_HTML5, 'UTF-8');
|
|
}
|
|
|
|
/**
|
|
* @return array{email:string,name:string}
|
|
*/
|
|
private function parseFrom(string $from): array
|
|
{
|
|
if (\preg_match('/^(.*)<([^>]+)>$/', \trim($from), $m)) {
|
|
return [
|
|
'name' => \trim($m[1], " \t\"'"),
|
|
'email' => \strtolower(\trim($m[2])),
|
|
];
|
|
}
|
|
|
|
$email = \strtolower(\trim($from));
|
|
|
|
return ['email' => $email, 'name' => ''];
|
|
}
|
|
}
|