Perl SMTP:无法在正文中发送包含非ascii字符的电子邮件

时间:2017-09-15 23:02:49

标签: perl smtp non-ascii-characters

代码,发送电子邮件(工作正常):

#!/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)>>> .

如何解决?

1 个答案:

答案 0 :(得分:2)

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

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

在将邮件发送给sendmail(...)之前。但请注意这个答案结尾处的警告,说明在邮件中首先发送8位数据时可能出现的问题。

要真正理解问题,修复者必须深入研究Perl中套接字中字符与八位字节的处理:

  • Email::Sender::Transport::SMTP使用Net::SMTP,它本身使用基础syswriteIO::Socket::SSL(或IO::Socket::IP)套接字的IO::Socket::INET方法,具体取决于是否使用了SSL。
  • syswrite期望八位字节,并且它期望写入套接字的八位字节数。
  • 但是,使用Email::Simple构造的邮件不返回八位字节,而是返回设置了UTF8标志的字符串。在此字符串中,字符数与八位字节数不同,因为俄语текст被视为5个字符,而使用UTF-8转换为10个八位字节。
  • Email::Sender::Transport::SMTP只是将电子邮件的UTF8字符串转发给Net::SMTPsyswritelength内使用它。使用ий\r\n.\r\n 计算长度,它给出的字符数与这种情况下的八位字节数不同。但是在套接字站点上,它将占用八位字节而不是字符串中的字符,并将给定长度视为八位字节数。
  • 因为它将给定长度视为八位字节而不是字符,所以它最终会向程序服务器上层发送较少的数据。
  • 这样,邮件结束标记(带有单点的行)就不会被发送,因此服务器正在等待客户端发送更多数据,而客户端不知道要发送更多数据。

作为一个例子,请使用仅包含两个俄语字符'йй'的邮件。对于行尾和邮件结束标记,它由7个字符组成:

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

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

syswrite($fd,"ий\r\n.\r\n",7)

现在,и й \r \n . d0 b8 d0 b9 0d 0a 2e 只会写出7个字符的前7个八位字节,但是9个八位字节长的字符串:

syswrite

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

现在,谁对此太过责备?

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

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

中明确说明了这一点
  

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

现在有人可能认为Email::Simple::as_string应该正确处理UTF8字符串,或者Email::MIME不应该首先返回UTF8字符串。

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