Skip to content

Commit b2358fd

Browse files
authored
Merge pull request #112 from utopia-php/feat-smtp-enhancements
feat: add string attachments, SMTP keep-alive, and command timelimit
2 parents fcb4c3c + 6988c22 commit b2358fd

File tree

3 files changed

+184
-18
lines changed

3 files changed

+184
-18
lines changed

src/Utopia/Messaging/Adapter/Email/SMTP.php

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class SMTP extends EmailAdapter
2020
* @param bool $smtpAutoTLS Enable/disable SMTP AutoTLS feature. Defaults to false.
2121
* @param string $xMailer The value to use for the X-Mailer header.
2222
* @param int $timeout SMTP timeout in seconds.
23+
* @param bool $keepAlive Whether to reuse the SMTP connection across process() calls.
24+
* @param int $timelimit SMTP command timelimit in seconds.
2325
*/
2426
public function __construct(
2527
private string $host,
@@ -29,13 +31,17 @@ public function __construct(
2931
private string $smtpSecure = '',
3032
private bool $smtpAutoTLS = false,
3133
private string $xMailer = '',
32-
private int $timeout = 30
34+
private int $timeout = 30,
35+
private bool $keepAlive = false,
36+
private int $timelimit = 30,
3337
) {
3438
if (!\in_array($this->smtpSecure, ['', 'ssl', 'tls'])) {
3539
throw new \InvalidArgumentException('Invalid SMTP secure prefix. Must be "", "ssl" or "tls"');
3640
}
3741
}
3842

43+
private ?PHPMailer $mail = null;
44+
3945
public function getName(): string
4046
{
4147
return static::NAME;
@@ -52,18 +58,33 @@ public function getMaxMessagesPerRequest(): int
5258
protected function process(EmailMessage $message): array
5359
{
5460
$response = new Response($this->getType());
55-
$mail = new PHPMailer();
56-
$mail->isSMTP();
61+
62+
if ($this->keepAlive && $this->mail !== null) {
63+
$mail = $this->mail;
64+
$mail->clearAllRecipients();
65+
$mail->clearReplyTos();
66+
$mail->clearAttachments();
67+
} else {
68+
$mail = new PHPMailer();
69+
$mail->isSMTP();
70+
$mail->Host = $this->host;
71+
$mail->Port = $this->port;
72+
$mail->SMTPAuth = !empty($this->username) && !empty($this->password);
73+
$mail->Username = $this->username;
74+
$mail->Password = $this->password;
75+
$mail->SMTPSecure = $this->smtpSecure;
76+
$mail->SMTPAutoTLS = $this->smtpAutoTLS;
77+
$mail->Timeout = $this->timeout;
78+
$mail->SMTPKeepAlive = $this->keepAlive;
79+
80+
if ($this->keepAlive) {
81+
$this->mail = $mail;
82+
}
83+
}
84+
5785
$mail->XMailer = $this->xMailer;
58-
$mail->Host = $this->host;
59-
$mail->Port = $this->port;
60-
$mail->SMTPAuth = !empty($this->username) && !empty($this->password);
61-
$mail->Username = $this->username;
62-
$mail->Password = $this->password;
63-
$mail->SMTPSecure = $this->smtpSecure;
64-
$mail->SMTPAutoTLS = $this->smtpAutoTLS;
65-
$mail->Timeout = $this->timeout;
6686
$mail->CharSet = 'UTF-8';
87+
$mail->getSMTPInstance()->Timelimit = $this->timelimit;
6788
$mail->Subject = $message->getSubject();
6889
$mail->Body = $message->getContent();
6990
$mail->setFrom($message->getFromEmail(), $message->getFromName());
@@ -95,19 +116,33 @@ protected function process(EmailMessage $message): array
95116
$size = 0;
96117

97118
foreach ($message->getAttachments() as $attachment) {
98-
$size += \filesize($attachment->getPath());
119+
if ($attachment->getContent() !== null) {
120+
$size += \strlen($attachment->getContent());
121+
} else {
122+
$size += \filesize($attachment->getPath());
123+
}
99124
}
100125

101126
if ($size > self::MAX_ATTACHMENT_BYTES) {
102127
throw new \Exception('Attachments size exceeds the maximum allowed size of 25MB');
103128
}
104129

105130
foreach ($message->getAttachments() as $attachment) {
106-
$mail->addStringAttachment(
107-
string: \file_get_contents($attachment->getPath()),
108-
filename: $attachment->getName(),
109-
type: $attachment->getType()
110-
);
131+
if ($attachment->getContent() !== null) {
132+
$mail->addStringAttachment(
133+
string: $attachment->getContent(),
134+
filename: $attachment->getName(),
135+
encoding: PHPMailer::ENCODING_BASE64,
136+
type: $attachment->getType()
137+
);
138+
} else {
139+
$mail->addStringAttachment(
140+
string: \file_get_contents($attachment->getPath()),
141+
filename: $attachment->getName(),
142+
encoding: PHPMailer::ENCODING_BASE64,
143+
type: $attachment->getType()
144+
);
145+
}
111146
}
112147
}
113148

src/Utopia/Messaging/Messages/Email/Attachment.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ class Attachment
66
{
77
/**
88
* @param string $name The name of the file.
9-
* @param string $path The content of the file.
9+
* @param string $path The path of the file.
1010
* @param string $type The MIME type of the file.
11+
* @param ?string $content Raw string content of the file (used instead of path when non-null).
1112
*/
1213
public function __construct(
1314
private string $name,
1415
private string $path,
1516
private string $type,
17+
private ?string $content = null,
1618
) {
1719
}
1820

@@ -30,4 +32,9 @@ public function getType(): string
3032
{
3133
return $this->type;
3234
}
35+
36+
public function getContent(): ?string
37+
{
38+
return $this->content;
39+
}
3340
}

tests/Messaging/Adapter/Email/SMTPTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,128 @@ public function testSendEmailOnlyBCC(): void
114114
$this->assertEquals($subject, $lastEmail['subject']);
115115
$this->assertEquals($content, \trim($lastEmail['text']));
116116
}
117+
118+
public function testAttachmentWithStringContent(): void
119+
{
120+
$content = 'Hello, this is raw file content.';
121+
$attachment = new Attachment(
122+
name: 'readme.txt',
123+
path: '',
124+
type: 'text/plain',
125+
content: $content,
126+
);
127+
128+
$this->assertEquals('readme.txt', $attachment->getName());
129+
$this->assertEquals('', $attachment->getPath());
130+
$this->assertEquals('text/plain', $attachment->getType());
131+
$this->assertEquals($content, $attachment->getContent());
132+
}
133+
134+
public function testAttachmentWithoutStringContentDefaultsToNull(): void
135+
{
136+
$attachment = new Attachment(
137+
name: 'image.png',
138+
path: '/tmp/image.png',
139+
type: 'image/png',
140+
);
141+
142+
$this->assertNull($attachment->getContent());
143+
}
144+
145+
public function testSMTPConstructorWithKeepAliveAndTimelimit(): void
146+
{
147+
$sender = new SMTP(
148+
host: 'maildev',
149+
port: 1025,
150+
keepAlive: true,
151+
timelimit: 60,
152+
);
153+
154+
$this->assertInstanceOf(SMTP::class, $sender);
155+
$this->assertEquals('SMTP', $sender->getName());
156+
}
157+
158+
public function testSMTPConstructorDefaultsAreBackwardsCompatible(): void
159+
{
160+
// Existing call signature still works without new params
161+
$sender = new SMTP(
162+
host: 'maildev',
163+
port: 1025,
164+
);
165+
166+
$this->assertInstanceOf(SMTP::class, $sender);
167+
}
168+
169+
public function testSendEmailWithStringAttachment(): void
170+
{
171+
$sender = new SMTP(
172+
host: 'maildev',
173+
port: 1025,
174+
);
175+
176+
$to = 'tester@localhost.test';
177+
$subject = 'String Attachment Test';
178+
$content = 'Test with string attachment';
179+
$fromName = 'Test Sender';
180+
$fromEmail = 'sender@localhost.test';
181+
182+
$message = new Email(
183+
to: [$to],
184+
subject: $subject,
185+
content: $content,
186+
fromName: $fromName,
187+
fromEmail: $fromEmail,
188+
attachments: [new Attachment(
189+
name: 'note.txt',
190+
path: '',
191+
type: 'text/plain',
192+
content: 'This is inline content',
193+
)],
194+
);
195+
196+
$response = $sender->send($message);
197+
198+
$lastEmail = $this->getLastEmail();
199+
200+
$this->assertResponse($response);
201+
$this->assertEquals($to, $lastEmail['to'][0]['address']);
202+
$this->assertEquals($subject, $lastEmail['subject']);
203+
}
204+
205+
public function testSendEmailWithKeepAlive(): void
206+
{
207+
$sender = new SMTP(
208+
host: 'maildev',
209+
port: 1025,
210+
keepAlive: true,
211+
timelimit: 15,
212+
);
213+
214+
$to = 'tester@localhost.test';
215+
$fromEmail = 'sender@localhost.test';
216+
217+
// Send first message
218+
$message1 = new Email(
219+
to: [$to],
220+
subject: 'KeepAlive Test 1',
221+
content: 'First message',
222+
fromName: 'Test',
223+
fromEmail: $fromEmail,
224+
);
225+
226+
$response1 = $sender->send($message1);
227+
$this->assertResponse($response1);
228+
229+
// Send second message — should reuse the PHPMailer instance
230+
$message2 = new Email(
231+
to: [$to],
232+
subject: 'KeepAlive Test 2',
233+
content: 'Second message',
234+
fromName: 'Test',
235+
fromEmail: $fromEmail,
236+
);
237+
238+
$response2 = $sender->send($message2);
239+
$this->assertResponse($response2);
240+
}
117241
}

0 commit comments

Comments
 (0)