Passed
Push — master ( e7acbe...c65cd9 )
by Esteban De La Fuente
19:00
created

ImapReceiverStrategy::canReceive()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * LibreDTE: Biblioteca PHP (Núcleo).
7
 * Copyright (C) LibreDTE <https://www.libredte.cl>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (a su elección) cualquier versión posterior de la misma.
13
 *
14
 * Este programa se distribuye con la esperanza de que sea útil, pero SIN
15
 * GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD
16
 * PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de
20
 * GNU junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace libredte\lib\Core\Package\Billing\Component\Exchange\Worker\Receiver\Strategy\Email;
26
27
use DateTimeImmutable;
28
use Derafu\Lib\Core\Foundation\Abstract\AbstractStrategy;
29
use Derafu\Lib\Core\Helper\Str;
30
use Derafu\Lib\Core\Package\Prime\Component\Mail\Contract\EnvelopeInterface as MailEnvelopeInterface;
31
use Derafu\Lib\Core\Package\Prime\Component\Mail\Contract\MailComponentInterface;
32
use Derafu\Lib\Core\Package\Prime\Component\Mail\Contract\MessageInterface as MailMessageInterface;
33
use Derafu\Lib\Core\Package\Prime\Component\Mail\Support\Postman as MailPostman;
34
use libredte\lib\Core\Package\Billing\Component\Exchange\Contract\EnvelopeInterface;
35
use libredte\lib\Core\Package\Billing\Component\Exchange\Contract\ExchangeBagInterface;
36
use libredte\lib\Core\Package\Billing\Component\Exchange\Contract\ReceiverStrategyInterface;
37
use libredte\lib\Core\Package\Billing\Component\Exchange\Entity\PartyIdentifier;
38
use libredte\lib\Core\Package\Billing\Component\Exchange\Entity\Receiver;
39
use libredte\lib\Core\Package\Billing\Component\Exchange\Entity\Sender;
40
use libredte\lib\Core\Package\Billing\Component\Exchange\Exception\ExchangeException;
41
use libredte\lib\Core\Package\Billing\Component\Exchange\Support\Attachment;
42
use libredte\lib\Core\Package\Billing\Component\Exchange\Support\Document;
43
use libredte\lib\Core\Package\Billing\Component\Exchange\Support\Envelope;
44
use libredte\lib\Core\Package\Billing\Component\Exchange\Support\ExchangeResult;
45
use Symfony\Component\Mailer\Envelope as SymfonyEnvelope;
46
use Symfony\Component\Mime\Email as SymfonyEmail;
47
48
/**
49
 * Recepción de documentos usando la estrategia IMAP de correo electrónico.
50
 */
51
class ImapReceiverStrategy extends AbstractStrategy implements ReceiverStrategyInterface
52
{
53
    /**
54
     * Constructor y sus dependencias.
55
     *
56
     * @param MailComponentInterface $mailComponent
57
     */
58
    public function __construct(private MailComponentInterface $mailComponent)
59
    {
60
    }
61
62
    /**
63
     * {@inheritDoc}
64
     */
65
    public function receive(ExchangeBagInterface $bag): array
66
    {
67
        // Crear el cartero con las opciones.
68
        $postman = new MailPostman([
69
            'strategy' => 'imap',
70
            'transport' => $this->resolveTransportOptions($bag),
71
        ]);
72
73
        // Obtener los sobres con los correos electrónicos.
74
        $mailEnvelopes = $this->mailComponent->receive($postman);
75
76
        // Iterar los sobres para armar los sobres de intercambio con los
77
        // documentos que se hayan encontrado en los mensajes.
78
        foreach ($mailEnvelopes as $mailEnvelope) {
79
            assert($mailEnvelope instanceof SymfonyEnvelope);
80
81
            // Iterar cada mensaje del sobre. La implementación de MailComponent
82
            // que viene por defecto en Derafu entrega solo un mensaje por cada
83
            // sobre. Sin embargo, cada mensaje podría tener múltiples adjuntos,
84
            // pues eso lo decide quien envía el correo.
85
            foreach ($mailEnvelope->getMessages() as $message) {
86
                assert($message instanceof SymfonyEmail);
87
88
                // Revisar cada adjunto del mensaje y agregarlo al listado si es
89
                // un XML, pues los XML se deberán considerar como documentos
90
                // del proceso de intercambio.
91
                $attachments = $this->extractXmlAttachments($message);
92
                if (empty($attachments)) {
93
                    continue;
94
                }
95
96
                // Si hay archivos adjuntos encontrados en el mensaje se crea el
97
                // sobre de intercambio (diferente al de email) y se agregan los
98
                // documentos encontrados.
99
                $envelope = $this->createEnvelope(
100
                    $mailEnvelope,
101
                    $message,
102
                    $attachments
103
                );
104
105
                // Agregar el sobre a los resultados de la bolsa.
106
                $bag->addResult(new ExchangeResult($envelope));
107
            }
108
        }
109
110
        // Entregar los resultados de la recepción de documentos.
111
        return $bag->getResults();
112
    }
113
114
    /**
115
     * Crea el sobre de intercambio usando un sobre de correo, con un mensaje y
116
     * los adjuntos en XML que se encontraron en ese mensaje.
117
     *
118
     * @param MailEnvelopeInterface $mailEnvelope
119
     * @param MailMessageInterface $message
120
     * @param Attachment[] $attachments
121
     * @return EnvelopeInterface
122
     */
123
    private function createEnvelope(
124
        MailEnvelopeInterface $mailEnvelope,
125
        MailMessageInterface $message,
126
        array $attachments
127
    ): EnvelopeInterface {
128
        assert($mailEnvelope instanceof SymfonyEnvelope);
129
        assert($message instanceof SymfonyEmail);
130
131
        // Asignar el correo electrónico del remitente del mensaje.
132
        $sender = new Sender(new PartyIdentifier(
133
            ($message->getReplyTo()[0] ?? null)?->getAddress()
134
                ?? ($message->getFrom()[0] ?? null)?->getAddress()
135
                ?? $mailEnvelope->getSender()->getAddress(),
136
            'EMAIL'
137
        ));
138
139
        // Asignar el correo electrónico del receptor del mensaje.
140
        $receiver = new Receiver(new PartyIdentifier(
141
            ($message->getTo()[0] ?? null)?->getAddress()
142
                ?? ($message->getCc()[0] ?? null)?->getAddress()
143
                ?? ($message->getBcc()[0] ?? null)?->getAddress()
144
                ?? ($mailEnvelope->getRecipients()[0] ?? null)?->getAddress()
145
                ?? 'no-email',
146
            'EMAIL'
147
        ));
148
149
        // Crear el sobre de intercambio.
150
        $envelope = new Envelope(
151
            sender: $sender,
152
            receiver: $receiver,
153
            businessMessageID: (string) ($message->getId() ?: Str::uuid4()),
154
            creationDateAndTime: $message->getDate()
155
        );
156
157
        // Crear un documento por cada XML recibido y agregarlos al sobre.
158
        foreach ($attachments as $attachment) {
159
            $document = new Document();
160
            $document->addAttachment($attachment);
161
            $envelope->addDocument($document);
162
        }
163
164
        // Entregar el sobre con todos los documentos que venían adjuntos.
165
        return $envelope;
166
    }
167
168
    /**
169
     * Extrae de un mensaje de correo electrónico los archivos adjuntos que son
170
     * archivos XML.
171
     *
172
     * @param SymfonyEmail $message
173
     * @return Attachment[]
174
     */
175
    private function extractXmlAttachments(SymfonyEmail $message): array
176
    {
177
        // Si el mensaje no tiene adjuntos se entrega un arreglo vacio..
178
        $mailAttachments = $message->getAttachments();
179
        if (empty($mailAttachments)) {
180
            return [];
181
        }
182
183
        // Si el mensaje tiene adjuntos se buscan los que sean XML.
184
        $attachments = [];
185
        foreach ($mailAttachments as $mailAttachment) {
186
            if ($mailAttachment->getMediaSubtype() === 'xml') {
187
                $attachments[] = new Attachment(
188
                    $mailAttachment->getBody(),
189
                    $mailAttachment->getFilename(),
190
                    $mailAttachment->getMediaType() . '/'
191
                        . $mailAttachment->getMediaSubtype()
192
                );
193
            }
194
        }
195
196
        // Entregar archivos adjuntos encontrados, si es que se encontraron.
197
        return $attachments;
198
    }
199
200
    /**
201
     * {@inheritDoc}
202
     */
203
    public function canReceive(ExchangeBagInterface $bag): void
204
    {
205
        $this->resolveTransportOptions($bag);
206
    }
207
208
    /**
209
     * Resuelve y entrega los datos de transporte.
210
     *
211
     * @param ExchangeBagInterface $bag
212
     * @return array
213
     * @throws ExchangeException Si no se encuentran los datos de transporte.
214
     */
215
    private function resolveTransportOptions(ExchangeBagInterface $bag): array
216
    {
217
        // Buscar opciones de transporte en la bolsa del intercambio.
218
        $options = $bag->getOptions()->get('transport', []);
219
220
        // Validar que esté el nombre de usuario y contraseña.
221
        if (empty($options['username']) || empty($options['password'])) {
222
            throw new ExchangeException(
223
                'Se debe especificar el usuario y contraseña de IMAP.'
224
            );
225
        }
226
227
        // Determinar desde cuándo se debe realizar la búsqueda de correos.
228
        if (($options['search']['criteria'] ?? null) === null) {
229
            $daysAgo = $options['search']['daysAgo'] ?? 7;
230
            $since = (new DateTimeImmutable())->modify("-$daysAgo days")->format('Y-m-d');
231
            $options['search']['criteria'] = 'UNSEEN SINCE ' . $since;
232
            unset($options['search']['daysAgo']);
233
        }
234
235
        // Si no se indicó lo contrario los correos serán marcados como leídos
236
        // después de ser procesados.
237
        if (!isset($options['search']['markAsSeen'])) {
238
            $options['search']['markAsSeen'] = true;
239
        }
240
241
        // Obtiene exclusivamente el cuerpo del correo (texto o html) y archivos
242
        // XML que vengan como adjuntos.
243
        $options['search']['attachmentFilters'] = [
244
            'subtype' => ['PLAIN', 'HTML', 'XML'],
245
            'extension' => ['xml'],
246
        ];
247
248
        // Entregar las opciones resueltas.
249
        return $options;
250
    }
251
}
252