【问题标题】:Perl SMTP: can't send email with non-ascii characters in bodyPerl SMTP:无法发送正文中包含非 ASCII 字符的电子邮件
【发布时间】:2018-02-25 04:05:44
【问题描述】:

代码,发送电子邮件(运行良好):

#!/usr/bin/perl

use utf8;
use strict;
use warnings;

use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::SMTP ();
use Email::Simple ();
use open ':std', ':encoding(UTF-8)';

sub send_email
{
    my $email_from = shift;
    my $email_to = shift;
    my $subject = shift;
    my $message = shift;

    my $smtpserver = 'smtp.gmail.com';
    my $smtpport = 465;
    my $smtpuser   = 'user@gmail.com';
    my $password = 'secret';

    my $transport = Email::Sender::Transport::SMTP->new({
        host => $smtpserver,
        port => $smtpport,
        sasl_username => $email_from,
        sasl_password => $password,
        debug    => 1,
        ssl => 1,
    });

    my $email = Email::Simple->create(
        header => [
            To      => $email_to,
            From    => $email_from,
            Subject => $subject,
        ],
        body => $message,
    );

    $email->header_set( 'Content-Type' => 'text/html' );
    $email->header_set( 'charset' => 'UTF-8' );
    sendmail($email, { transport => $transport });
}

send_email('user@gmail.com', 'user@gmail.com', 'Hello', 'test email');

只要我在正文中添加非 ascii 字符:

send_email('user@gmail.com', 'user@gmail.com', 'Hello', 'test email. Русский текст');

它与调试输出中的最后一条消息一起挂起:

Net::SMTP::_SSL=GLOB(0x8d41fa0)>>> charset: UTF-8
Net::SMTP::_SSL=GLOB(0x8d41fa0)>>> 
Net::SMTP::_SSL=GLOB(0x8d41fa0)>>> test email. Русский текст
Net::SMTP::_SSL=GLOB(0x8d41fa0)>>> .

如何解决?

【问题讨论】:

    标签: perl smtp non-ascii-characters


    【解决方案1】:

    TL;TR:修复很简单,但问题本身很复杂。要解决此问题,请添加:

    $email = Encode::encode('utf-8',$email->as_string)
    

    在将邮件发送至sendmail(...) 之前。但请注意此答案末尾的警告,即首先在邮件中发送此类 8 位数据时可能出现问题。


    要真正理解问题和解决方法,必须深入研究 Perl 中对字符与八位字节的处理:

    • Email::Sender::Transport::SMTP 使用Net::SMTP,它本身使用底层IO::Socket::SSLIO::Socket::IP(或IO::Socket::INET)套接字的syswrite 方法,具体取决于是否使用了SSL。
    • syswrite 需要八位字节并且它需要写入套接字的八位字节数。
    • 但是,您使用Email::Simple 构造的邮件返回的不是八位字节,而是一个设置了UTF8 标志的字符串。在此字符串中,字符数与八位字节数不同,因为俄语 текст 被视为 5 个字符,而使用 UTF-8 转换时为 10 个八位字节。
    • Email::Sender::Transport::SMTP 只是将电子邮件的 UTF8 字符串转发到 Net::SMTP,后者在 syswrite 中使用它。长度是使用length 计算的,它给出的字符数与本例中的八位字节数不同。但是在套接字站点上,它将从字符串中取出八位字节而不是字符,并将给定的长度视为八位字节的数量。
    • 因为它将给定的长度视为八位字节而不是字符,它最终会像程序的上层所期望的那样向服务器发送更少的数据。
    • 这样,邮件结束标记(单点线)不会发送,因此服务器正在等待客户端发送更多数据,而客户端不知道要发送更多数据。

    以仅包含两个俄语字符“ий”的邮件为例。行尾和邮件结束标记由 7 个字符组成:

    ий\r\n.\r\n
    

    但是,这 7 个字符实际上是 9 个八位字节,因为前 2 个字符每个都是两个八位字节

    и       й       \r \n   .   \r  \n
    d0 b8   d0 b9   0d  0a  2e  0d  0a  
    

    现在,syswrite($fd,"ий\r\n.\r\n",7) 将只写入 7 个字符的前 7 个八位字节,但长字符串为 9 个八位字节:

    и       й       \r \n   . 
    d0 b8   d0 b9   0d  0a  2e
    

    这意味着邮件结束标记不完整。这意味着邮件服务器将等待更多数据,而邮件客户端不知道它需要发送的任何更多数据。这实际上会导致应用程序挂起。

    现在,这也怪谁?

    有人可能会争辩说 IO::Socket::SSL::syswrite 应该以一种理智的方式处理 UTF8 数据,而这正是 RT#98732 中所要求的。但是,IO::Socket::SSL 中syswrite 的文档清楚地表明它适用于字节。并且由于在考虑非阻塞套接字时几乎不可能创建基于字符的健全行为,因此该错误被拒绝。此外,非 SSL 套接字也会出现 UTF8 字符串的问题:如果您一开始不使用 SSL,程序不会挂起,而是会以 Wide character in syswrite ... 崩溃。

    下一层将期望 Net::SMTP 正确处理此类 UTF8 字符串。只是,在documentation of Net::SMTP::data中明确表示:

    DATA 可以是对列表或列表的引用,并且必须由调用者编码为八位字节,无论需要何种编码,例如通过使用 Encode 模块的 encode() 函数。

    现在有人可能会争辩说 Email::Transport 应该正确处理 UTF8 字符串,或者 Email::Simple::as_string 首先不应该返回 UTF8 字符串。

    但我们甚至可以更上一层楼:开发者本身。邮件传统上只是 ASCII,在邮件中发送非 ASCII 字符是个坏主意,因为它只能在具有 8BITMIME 扩展名的邮件服务器上可靠地工作。如果涉及不支持此扩展的邮件服务器,则结果是不可预测的,即邮件可以被转换(这可能会破坏签名),可以更改为不可读或可能在某处丢失。因此最好使用更复杂的模块,如Email::MIME,并设置适当的内容传输编码。

    【讨论】:

    • 非常感谢,您刚刚解决了我长达 2 小时的问题,并详细解释了它!
    猜你喜欢
    • 1970-01-01
    • 2022-07-20
    • 2020-08-06
    • 2013-01-24
    • 2019-01-23
    • 2017-02-15
    • 2020-03-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多