Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 52 additions & 17 deletions src/Utopia/Messaging/Adapter/Email/SMTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class SMTP extends EmailAdapter
* @param bool $smtpAutoTLS Enable/disable SMTP AutoTLS feature. Defaults to false.
* @param string $xMailer The value to use for the X-Mailer header.
* @param int $timeout SMTP timeout in seconds.
* @param bool $keepAlive Whether to reuse the SMTP connection across process() calls.
* @param int $timelimit SMTP command timelimit in seconds.
*/
public function __construct(
private string $host,
Expand All @@ -29,13 +31,17 @@ public function __construct(
private string $smtpSecure = '',
private bool $smtpAutoTLS = false,
private string $xMailer = '',
private int $timeout = 30
private int $timeout = 30,
private bool $keepAlive = false,
private int $timelimit = 30,
) {
if (!\in_array($this->smtpSecure, ['', 'ssl', 'tls'])) {
throw new \InvalidArgumentException('Invalid SMTP secure prefix. Must be "", "ssl" or "tls"');
}
}

private ?PHPMailer $mail = null;

public function getName(): string
{
return static::NAME;
Expand All @@ -52,18 +58,33 @@ public function getMaxMessagesPerRequest(): int
protected function process(EmailMessage $message): array
{
$response = new Response($this->getType());
$mail = new PHPMailer();
$mail->isSMTP();

if ($this->keepAlive && $this->mail !== null) {
$mail = $this->mail;
$mail->clearAllRecipients();
$mail->clearReplyTos();
$mail->clearAttachments();
} else {
$mail = new PHPMailer();
$mail->isSMTP();
$mail->Host = $this->host;
$mail->Port = $this->port;
$mail->SMTPAuth = !empty($this->username) && !empty($this->password);
$mail->Username = $this->username;
$mail->Password = $this->password;
$mail->SMTPSecure = $this->smtpSecure;
$mail->SMTPAutoTLS = $this->smtpAutoTLS;
$mail->Timeout = $this->timeout;
$mail->SMTPKeepAlive = $this->keepAlive;

if ($this->keepAlive) {
$this->mail = $mail;
}
}

$mail->XMailer = $this->xMailer;
$mail->Host = $this->host;
$mail->Port = $this->port;
$mail->SMTPAuth = !empty($this->username) && !empty($this->password);
$mail->Username = $this->username;
$mail->Password = $this->password;
$mail->SMTPSecure = $this->smtpSecure;
$mail->SMTPAutoTLS = $this->smtpAutoTLS;
$mail->Timeout = $this->timeout;
$mail->CharSet = 'UTF-8';
$mail->getSMTPInstance()->Timelimit = $this->timelimit;
$mail->Subject = $message->getSubject();
$mail->Body = $message->getContent();
$mail->setFrom($message->getFromEmail(), $message->getFromName());
Expand Down Expand Up @@ -95,19 +116,33 @@ protected function process(EmailMessage $message): array
$size = 0;

foreach ($message->getAttachments() as $attachment) {
$size += \filesize($attachment->getPath());
if ($attachment->getContent() !== null) {
$size += \strlen($attachment->getContent());
} else {
$size += \filesize($attachment->getPath());
}
}

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

foreach ($message->getAttachments() as $attachment) {
$mail->addStringAttachment(
string: \file_get_contents($attachment->getPath()),
filename: $attachment->getName(),
type: $attachment->getType()
);
if ($attachment->getContent() !== null) {
$mail->addStringAttachment(
string: $attachment->getContent(),
filename: $attachment->getName(),
encoding: PHPMailer::ENCODING_BASE64,
type: $attachment->getType()
);
} else {
$mail->addStringAttachment(
string: \file_get_contents($attachment->getPath()),
filename: $attachment->getName(),
encoding: PHPMailer::ENCODING_BASE64,
type: $attachment->getType()
);
}
}
}

Expand Down
9 changes: 8 additions & 1 deletion src/Utopia/Messaging/Messages/Email/Attachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ class Attachment
{
/**
* @param string $name The name of the file.
* @param string $path The content of the file.
* @param string $path The path of the file.
* @param string $type The MIME type of the file.
* @param ?string $content Raw string content of the file (used instead of path when non-null).
*/
public function __construct(
private string $name,
private string $path,
private string $type,
private ?string $content = null,
) {
}

Expand All @@ -30,4 +32,9 @@ public function getType(): string
{
return $this->type;
}

public function getContent(): ?string
{
return $this->content;
}
}
124 changes: 124 additions & 0 deletions tests/Messaging/Adapter/Email/SMTPTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,128 @@ public function testSendEmailOnlyBCC(): void
$this->assertEquals($subject, $lastEmail['subject']);
$this->assertEquals($content, \trim($lastEmail['text']));
}

public function testAttachmentWithStringContent(): void
{
$content = 'Hello, this is raw file content.';
$attachment = new Attachment(
name: 'readme.txt',
path: '',
type: 'text/plain',
content: $content,
);

$this->assertEquals('readme.txt', $attachment->getName());
$this->assertEquals('', $attachment->getPath());
$this->assertEquals('text/plain', $attachment->getType());
$this->assertEquals($content, $attachment->getContent());
}

public function testAttachmentWithoutStringContentDefaultsToNull(): void
{
$attachment = new Attachment(
name: 'image.png',
path: '/tmp/image.png',
type: 'image/png',
);

$this->assertNull($attachment->getContent());
}

public function testSMTPConstructorWithKeepAliveAndTimelimit(): void
{
$sender = new SMTP(
host: 'maildev',
port: 1025,
keepAlive: true,
timelimit: 60,
);

$this->assertInstanceOf(SMTP::class, $sender);
$this->assertEquals('SMTP', $sender->getName());
}

public function testSMTPConstructorDefaultsAreBackwardsCompatible(): void
{
// Existing call signature still works without new params
$sender = new SMTP(
host: 'maildev',
port: 1025,
);

$this->assertInstanceOf(SMTP::class, $sender);
}

public function testSendEmailWithStringAttachment(): void
{
$sender = new SMTP(
host: 'maildev',
port: 1025,
);

$to = 'tester@localhost.test';
$subject = 'String Attachment Test';
$content = 'Test with string attachment';
$fromName = 'Test Sender';
$fromEmail = 'sender@localhost.test';

$message = new Email(
to: [$to],
subject: $subject,
content: $content,
fromName: $fromName,
fromEmail: $fromEmail,
attachments: [new Attachment(
name: 'note.txt',
path: '',
type: 'text/plain',
content: 'This is inline content',
)],
);

$response = $sender->send($message);

$lastEmail = $this->getLastEmail();

$this->assertResponse($response);
$this->assertEquals($to, $lastEmail['to'][0]['address']);
$this->assertEquals($subject, $lastEmail['subject']);
}

public function testSendEmailWithKeepAlive(): void
{
$sender = new SMTP(
host: 'maildev',
port: 1025,
keepAlive: true,
timelimit: 15,
);

$to = 'tester@localhost.test';
$fromEmail = 'sender@localhost.test';

// Send first message
$message1 = new Email(
to: [$to],
subject: 'KeepAlive Test 1',
content: 'First message',
fromName: 'Test',
fromEmail: $fromEmail,
);

$response1 = $sender->send($message1);
$this->assertResponse($response1);

// Send second message — should reuse the PHPMailer instance
$message2 = new Email(
to: [$to],
subject: 'KeepAlive Test 2',
content: 'Second message',
fromName: 'Test',
fromEmail: $fromEmail,
);

$response2 = $sender->send($message2);
$this->assertResponse($response2);
}
}
Loading