1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-log for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-log/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-log/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Log\Writer;
10
11use Laminas\Log\Exception;
12use Laminas\Log\Formatter\Simple as SimpleFormatter;
13use Laminas\Mail\Message as MailMessage;
14use Laminas\Mail\MessageFactory as MailMessageFactory;
15use Laminas\Mail\Transport;
16use Laminas\Mail\Transport\Exception as TransportException;
17use Traversable;
18
19/**
20 * Class used for writing log messages to email via Laminas\Mail.
21 *
22 * Allows for emailing log messages at and above a certain level via a
23 * Laminas\Mail\Message object.  Note that this class only sends the email upon
24 * completion, so any log entries accumulated are sent in a single email.
25 * The email is sent using a Laminas\Mail\Transport\TransportInterface object
26 * (Sendmail is default).
27 */
28class Mail extends AbstractWriter
29{
30    /**
31     * Array of formatted events to include in message body.
32     *
33     * @var array
34     */
35    protected $eventsToMail = [];
36
37    /**
38     * Mail message instance to use
39     *
40     * @var MailMessage
41     */
42    protected $mail;
43
44    /**
45     * Mail transport instance to use; optional.
46     *
47     * @var Transport\TransportInterface
48     */
49    protected $transport;
50
51    /**
52     * Array keeping track of the number of entries per priority level.
53     *
54     * @var array
55     */
56    protected $numEntriesPerPriority = [];
57
58    /**
59     * Subject prepend text.
60     *
61     * Can only be used of the Laminas\Mail object has not already had its
62     * subject line set.  Using this will cause the subject to have the entry
63     * counts per-priority level appended to it.
64     *
65     * @var string|null
66     */
67    protected $subjectPrependText;
68
69    /**
70     * Constructor
71     *
72     * @param  MailMessage|array|Traversable $mail
73     * @param  Transport\TransportInterface $transport Optional
74     * @throws Exception\InvalidArgumentException
75     */
76    public function __construct($mail, Transport\TransportInterface $transport = null)
77    {
78        if ($mail instanceof Traversable) {
79            $mail = iterator_to_array($mail);
80        }
81
82        if (is_array($mail)) {
83            parent::__construct($mail);
84            if (isset($mail['subject_prepend_text'])) {
85                $this->setSubjectPrependText($mail['subject_prepend_text']);
86            }
87            $transport = isset($mail['transport']) ? $mail['transport'] : null;
88            $mail      = isset($mail['mail']) ? $mail['mail'] : null;
89            if (is_array($mail)) {
90                $mail = MailMessageFactory::getInstance($mail);
91            }
92            if (is_array($transport)) {
93                $transport = Transport\Factory::create($transport);
94            }
95        }
96
97        // Ensure we have a valid mail message
98        if (! $mail instanceof MailMessage) {
99            throw new Exception\InvalidArgumentException(sprintf(
100                'Mail parameter of type %s is invalid; must be of type Laminas\Mail\Message',
101                (is_object($mail) ? get_class($mail) : gettype($mail))
102            ));
103        }
104        $this->mail = $mail;
105
106        // Ensure we have a valid mail transport
107        if (null === $transport) {
108            $transport = new Transport\Sendmail();
109        }
110        if (! $transport instanceof Transport\TransportInterface) {
111            throw new Exception\InvalidArgumentException(sprintf(
112                'Transport parameter of type %s is invalid; must be of type Laminas\Mail\Transport\TransportInterface',
113                (is_object($transport) ? get_class($transport) : gettype($transport))
114            ));
115        }
116        $this->setTransport($transport);
117
118        if ($this->formatter === null) {
119            $this->formatter = new SimpleFormatter();
120        }
121    }
122
123    /**
124     * Set the transport message
125     *
126     * @param  Transport\TransportInterface $transport
127     * @return Mail
128     */
129    public function setTransport(Transport\TransportInterface $transport)
130    {
131        $this->transport = $transport;
132        return $this;
133    }
134
135    /**
136     * Places event line into array of lines to be used as message body.
137     *
138     * @param array $event Event data
139     */
140    protected function doWrite(array $event)
141    {
142        // Track the number of entries per priority level.
143        if (! isset($this->numEntriesPerPriority[$event['priorityName']])) {
144            $this->numEntriesPerPriority[$event['priorityName']] = 1;
145        } else {
146            $this->numEntriesPerPriority[$event['priorityName']]++;
147        }
148
149        // All plaintext events are to use the standard formatter.
150        $this->eventsToMail[] = $this->formatter->format($event);
151    }
152
153    /**
154     * Allows caller to have the mail subject dynamically set to contain the
155     * entry counts per-priority level.
156     *
157     * Sets the text for use in the subject, with entry counts per-priority
158     * level appended to the end.  Since a Laminas\Mail\Message subject can only be set
159     * once, this method cannot be used if the Laminas\Mail\Message object already has a
160     * subject set.
161     *
162     * @param  string $subject Subject prepend text
163     * @return Mail
164     */
165    public function setSubjectPrependText($subject)
166    {
167        $this->subjectPrependText = (string) $subject;
168        return $this;
169    }
170
171    /**
172     * Sends mail to recipient(s) if log entries are present.  Note that both
173     * plaintext and HTML portions of email are handled here.
174     */
175    public function shutdown()
176    {
177        // If there are events to mail, use them as message body.  Otherwise,
178        // there is no mail to be sent.
179        if (empty($this->eventsToMail)) {
180            return;
181        }
182
183        if ($this->subjectPrependText !== null) {
184            // Tack on the summary of entries per-priority to the subject
185            // line and set it on the Laminas\Mail object.
186            $numEntries = $this->getFormattedNumEntriesPerPriority();
187            $this->mail->setSubject("{$this->subjectPrependText} ({$numEntries})");
188        }
189
190        // Always provide events to mail as plaintext.
191        $this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));
192
193        // Finally, send the mail.  If an exception occurs, convert it into a
194        // warning-level message so we can avoid an exception thrown without a
195        // stack frame.
196        try {
197            $this->transport->send($this->mail);
198        } catch (TransportException\ExceptionInterface $e) {
199            trigger_error(
200                "unable to send log entries via email; " .
201                "message = {$e->getMessage()}; " .
202                "code = {$e->getCode()}; " .
203                "exception class = " . get_class($e),
204                E_USER_WARNING
205            );
206        }
207    }
208
209    /**
210     * Gets a string of number of entries per-priority level that occurred, or
211     * an empty string if none occurred.
212     *
213     * @return string
214     */
215    protected function getFormattedNumEntriesPerPriority()
216    {
217        $strings = [];
218
219        foreach ($this->numEntriesPerPriority as $priority => $numEntries) {
220            $strings[] = "{$priority}={$numEntries}";
221        }
222
223        return implode(', ', $strings);
224    }
225}
226