2017-09-15 39 views
1

代码,发送电子邮件(工作好):Perl的SMTP:使用非ASCII字符的身体无法发送电子邮件

#!/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 = '[email protected]'; 
    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('[email protected]', '[email protected]', 'Hello', 'test email'); 

只要我非ASCII字符添加到身体:

send_email('[email protected]', '[email protected]', '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

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

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

将邮件发送给sendmail(...)之前。但请注意,在回复结束时,警告在发送邮件时首先发送像这样的8位数据时可能出现的问题。


要真正理解这个问题,一个有更深入了解人物与八位位组的处理在插口在Perl的修补程序:

  • Email::Sender::Transport::SMTP使用Net::SMTP其本身使用的的syswrite方法底层IO::Socket::SSLIO::Socket::IP(或IO::Socket::INET)套接字,具体取决于是否使用了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,并设置适当的内容传输编码。