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:
@@ -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');
|
||||
|
||||
@@ -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
48
include/class.mime.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user