mail: SMTPUTF8 Backwards Compatibility

This adds backwards compatibility support for legacy mail servers that
don’t support SMTPUTF8 or ones that simply refuse to support it (get
with the times old man!). When the system adds an attachment or adds an
inline image to outbound mail we add the raw UTF-8 filename to the
`filename=` parameter contained within the `Content-Disposition` header.
This can be problematic with legacy mail servers or servers that refuse
to support SMTPUTF8 as non-ASCII characters contained within the
`filename=` parameter of the `Content-Disposition` header triggers
SMTPUTF8 requirement. If the server doesn’t support it but it’s required
then it bounces the mail, typically with an error message stating such.

This updates the `Message::addInlineImage()` and
`Message::addAttachment()` methods to not only provide an ASCII-safe
filename but to also utilize a new MIME Part `Rfc2231Part` (discussed
momentarily). To generate said ASCII-safe filenames for both inline
images and any attached files this adds a new method called
`Message::asciiFallback()` that converts raw UTF-8 filenames to ASCII as
well as removes any non-header safe characters. In addition, this adds a
new MIME utility namespace `osTicket\Mime` that houses a new MIME
utility called `Rfc2231Part`. This class extends `Laminas\Mime\Part` to
essentially modify the `Content-Disposition` header and tack on the
original UTF-8 filename using the `filename*=UTF-8''` parameter but
encoded with `rawurlencode()` to make it ASCII-safe.

In summary, this adds an ASCII-safe `filename=` parameter to the
`Content-Disposition` header to prevent legacy servers from triggering
SMTPUTF8 requirement whilst utilizing `Rfc2231Part` to additionally
provide an (ASCII-safe) UTF-8 `filename*=UTF-8''` parameter for modern
mail servers (preserving the original filename).
This commit is contained in:
JediKev
2025-10-14 14:04:20 -05:00
parent 3ff35ad534
commit 05b87199ac
3 changed files with 71 additions and 9 deletions

View File

@@ -17,6 +17,7 @@ include_once(INCLUDE_DIR.'class.dept.php');
include_once(INCLUDE_DIR.'class.mail.php');
include_once(INCLUDE_DIR.'class.mailer.php');
include_once(INCLUDE_DIR.'class.oauth2.php');
include_once(INCLUDE_DIR.'class.mime.php');
include_once(INCLUDE_DIR.'class.mailfetch.php');
include_once(INCLUDE_DIR.'class.mailparse.php');
include_once(INCLUDE_DIR.'api.tickets.php');

View File

@@ -23,6 +23,7 @@ namespace osTicket\Mail {
use Laminas\Mime\Part as MimePart;
use Laminas\Mail\Header;
use osTicket\Mail\Header\ReturnPath;
use osTicket\Mime\Rfc2231Part;
class Message extends MailMessage {
// Message Id (mid)
@@ -120,24 +121,28 @@ namespace osTicket\Mail {
}
public function addInlineImage($id, $file) {
$f = new MimePart($file->getData());
$name = $file->getName();
$asciiName = self::asciiFallback($name);
$f = new Rfc2231Part($file->getData());
$f->id = $id;
$f->type = sprintf('%s; name="%s"',
$file->getMimeType(),
$file->getName());
$f->filename = $file->getName();
$f->disposition = Mime::DISPOSITION_INLINE;
$f->type = $file->getMimeType();
$f->encoding = Mime::ENCODING_BASE64;
$f->disposition = Mime::DISPOSITION_INLINE;
$f->filename = $asciiName;
$f->setRawFilename($name);
$this->addMimePart($f);
$this->hasInlineImages = true;
}
public function addAttachment($file, $name=null) {
$f = new MimePart($file->getData());
$name = $name ?: $file->getName();
$asciiName = self::asciiFallback($name);
$f = new Rfc2231Part($file->getData());
$f->type = $file->getMimeType();
$f->filename = $name ?: $file->getName();
$f->disposition = Mime::DISPOSITION_ATTACHMENT;
$f->encoding = Mime::ENCODING_BASE64;
$f->disposition = Mime::DISPOSITION_ATTACHMENT;
$f->filename = $asciiName;
$f->setRawFilename($name);
$this->addMimePart($f);
$this->hasAttachments = true;
}
@@ -286,6 +291,14 @@ namespace osTicket\Mail {
$this->setBody();
}
private static function asciiFallback(string $utf8): string {
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $utf8);
$ascii = $ascii !== false ? $ascii : '';
// Sanitize to header-safe ASCII
$ascii = preg_replace('/[^A-Za-z0-9._-]+/', '_', $ascii ?? '') ?? '';
return $ascii !== '' ? $ascii : $utf8;
}
}
// This is a wrapper class for Mime/Message that generates multipart

48
include/class.mime.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/**
* class.mime.php
*
* osTicket MIME utilities — extensions for Laminas\Mime components.
*
* @author JediKev <kevin@enhancesoft.com>
* @copyright Copyright (c) osTicket <gpl@osticket.com>
*
*/
namespace osTicket\Mime {
use Laminas\Mime\Mime;
use Laminas\Mime\Part;
class Rfc2231Part extends Part {
private ?string $rawfilename = null;
/**
* Store the raw (UTF-8) filename for RFC-2231 emission.
*/
public function setRawFilename(string $name): void {
$this->rawfilename = $name;
}
/**
* Override to append ; filename*=UTF-8''<percent-encoded> to Content-Disposition.
* Keep everything else from the parent as-is.
*/
public function getHeadersArray($eol = Mime::LINEEND): array {
$headers = parent::getHeadersArray($eol);
if ($this->rawfilename === null || $this->rawfilename === '')
return $headers;
// RFC 2231: UTF-8 then percent-encode (spaces as %20)
$encoded = rawurlencode($this->rawfilename);
foreach ($headers as &$h) {
if (strcasecmp($h[0], 'Content-Disposition') === 0) {
$h[1] .= "; filename*=UTF-8''{$encoded}";
break;
}
}
unset($h);
return $headers;
}
}
}