电子邮件的不完整总结
电子邮件 是最早的互联网应用之一。
- Electronic Mail
- 电子的 邮件
- 电子邮件
- 电邮
组成
一个 电子邮件系统 至少由三部分组成
- mua
- 使用 smtp 和 mta 通讯
- 使用 imap 或 pop3 和 mda 通讯
- mta
- 和其它 mta 通讯时也是使用 smtp
- mda
- Mail Delivery Agent (邮件分发代理)
- 虽然叫做分发代理,但实际上需要 mua 主动去拉取邮件
- 使用 imap 或 pop3 和 mua 通讯
- mua 就类似于浏览器, mta 和 mda 就类似于 http服务器
一封 电子邮件 由两个部分组成
- 信封
- 是指 smtp 信封 -> rfc 5321
- SMTP “信封”是客户端发送给邮件服务器的一组信息,说明电子邮件来自何处和将前往何处。
- SMTP 信封不同于电子邮件头和正文,对电子邮件收件人不可见。
- 在电子邮件传递过程中,每次传送到一个不同的服务器时,这个信封都会被丢弃并替换。
- 消息
- IMF -> rfc 5322
- 这是一种类似于 http 报文的格式,也是分为两部分, headers 和 body 。
- headers
- Date 这是发送的日期,格式看上去像是 rfc-2822
- From
- To
- Cc
- Bcc
- Subject
- Reply-To 这是发件人期望回复的地址
- MIME-Version 声明 MIME 版本 通常都是 1.0
- Content-Type 内容的类型
- Content-Transfer-Encoding 内容编码的方式
- 通常是 7bit 8bit binary base64
- 以 X- 开头的是自定义的首部
- 除了 from date to 是必须的之外,其它的可以为空
- To Cc Bcc Subject 还有 Reply-To 可以由用户编写,其它的通常都由程序生成
- body
- 根据 headers 的 Content-Type 通常分为三种
- text/plain
- text/html
- multipart/mixed
- 根据 headers 的 Content-Type 通常分为三种
一封 电子邮件地址 由三个部分组成
- 用户名
- @ 符号
- @ 符号 读作 at
- @ 的意思就是 who at where , user at domain
- 域名
总结一下就是 三种协议,一种格式
- SMTP
- IMAP
- POP3
- IMF (Internet Message Format)
如何搭建一个邮件服务器
单机的在不同用户间发送邮件
在局域网里自娱自乐
能收发外网的邮件
使用命令行发送邮件
交互式命令
- 适用于 nc telnet openssl
- 大致分为五个步骤
- 连接到 smtp 服务器
- nc smtp.qq.com 25
- telnet smtp.qq.com 25
- openssl s_client -connect smtp.qq.com:465
- 建立会话; helo
- 身份认证; auth login
- 理论上不登录也可以的,但除了本地测试的smtp服务器,不会有不需要登录的
- 账号密码需要转换成 base64
- AUTH LOGIN
- 输入 AUTH LOGIN 之后,按提示输入,转换成 base64 的账号密码
- 用户转换账号密码的命令,用 echo 输出时要忽略行尾的空格
echo -n "bootstrap@example.com" | base64 echo -n "SendEmails" | base64
- 例子
AUTH LOGIN Ym9vdHN0cmFwQGV4YW1wbGUuY29t U2VuZEVtYWlscw==
- AUTH PLAIN 换行
- AUTH PLAIN 之后,在下一行输入,转换成 base64 的账号密码
- 用户转换账号密码的命令,账号和密码前面都有一个
\0
printf "\0%s\0%s" "bootstrap@example.com" "SendEmails" | base64
- 例子
AUTH PLAIN AGJvb3RzdHJhcEBleGFtcGxlLmNvbQBTZW5kRW1haWxz
- AUTH PLAIN 不换行
- 和 AUTH PLAIN 在同一行输入,转换成 base64 的账号密码
- 用户转换账号密码的命令,账号和密码前面都有一个
\0
,字符串要以 auth 开头printf "auth\0%s\0%s" "bootstrap@example.com" "SendEmails" | base64
- 例子
AUTH PLAIN YXV0aABib290c3RyYXBAZXhhbXBsZS5jb20AU2VuZEVtYWlscw==
- AUTH LOGIN 大多数 smtp 服务器都支持, AUTH PLAIN 换行 这种方式 smtp.qq.com 就不支持了
- 发送邮件信封(发件人和收件人); MAIL FROM 和 RCPT TO
- MAIL FROM 只有一个
- RCPT TO 可以有很多个,包含 header 里的 to cc bcc 这些,只需要写地址就可以了,不用写用户名
- 发送邮件内容(邮件正文和附件); 以 DATA 开始 以 . 结束
- 关闭会话; QUIT
- 连接到 smtp 服务器
- 一个收件人的例子
HELO example.com AUTH LOGIN MAIL FROM:<alice@example.com> RCPT TO:<bob@example.com> DATA Date: Mon, 4 April 2022 From: Alice <alice@example.com> Subject: Eggs benedict casserole To: Bob <bob@example.com> Hi Bob, I will bring the eggs benedict casserole recipe on Friday. -Alice . QUIT
- 多个个收件人的例子
HELO example.com AUTH LOGIN MAIL FROM:<alice@example.com> RCPT TO:<bob1@example.com> RCPT TO:<bob2@example.com> DATA Date: Mon, 4 April 2022 From: Alice <alice@example.com> Subject: Eggs benedict casserole To: Bob <bob@example.com> Hi Bob, I will bring the eggs benedict casserole recipe on Friday. -Alice . QUIT
- smtp 命令 NOOP HELP RSET
非交互式命令
- sendmail
- busybox 中的 sendmail
- busybox 中的 sendmail 可以在命令行里设置 smtp 地址和账号密码
-S
表示SMTP服务器的地址和端口号-au
表示发送邮箱名-ap
表示发送邮箱授权码
- 使用文件的例子
sendmail -f from@xx.com -t to@xx.com -S smtp.qq.com:465 -auxxx -apxxxx -s mail.txt
- 使用标准输入的例子
cat mail.txt | sendmail -f from@xx.com -t to@xx.com -S smtp.qq.com:465 -auxxx -apxxxx
- 使用标准输入的例子
cat <<- EOF | sendmail -f from@xx.com -t to@xx.com -S smtp.qq.com:465 -auxxx -apxxxx Date: Fri, 5 Apr 2024 06:27:52 +0000 To: bob1@example.com, bob2@example.com, bob3@example.com From: Mailer <alice@example.com> Subject: Here is the subject MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 This is the test message EOF
- busybox 中的 sendmail 可以在命令行里设置 smtp 地址和账号密码
- 普通的 sendmail
- 例子就是 上面的示例中删去 -S -au -ap 三个参数
- 一些版本的 sendmail 并不支持发信到其它 smtp 服务器
- 一些版本的 sendmail 则需要在配置文件里设置 smtp 地址和账号密码
- 一些版本的 sendmail 的版本不支持 -s 参数,只能从标准输入里读取
- busybox 中的 sendmail
- curl
- 从文件中读取邮件
curl -v --ssl --url 'smtps://smtp.qq.com:465/smtp.qq.com' \ --user 'alice@example.com:NvbQBTZW5kRW' \ --mail-from 'alice@example.com' \ --mail-rcpt 'bob@example.com' \ --login-options AUTH=LOGIN \ --upload-file mail-curl.txt
- 从标准输入中读取邮件
cat mail-curl.txt | curl -v --ssl --url 'smtps://smtp.qq.com:465/smtp.qq.com' \ --user 'alice@example.com:NvbQBTZW5kRW' \ --mail-from 'alice@example.com' \ --mail-rcpt 'bob@example.com' \ --login-options AUTH=LOGIN \ --upload-file -
- 从标准输入中读取邮件
cat <<- EOF | curl -v --no-progress-meter --url 'smtp://127.0.0.1:25/NB4045' \ --user 'alice@example.com:NvbQBTZW5kRW' \ --mail-from 'alice@example.com' \ --mail-rcpt 'bob1@example.com' \ --mail-rcpt 'bob2@example.com' \ --mail-rcpt 'bob3@example.com' \ --login-options AUTH=LOGIN \ --upload-file - \ Date: Fri, 5 Apr 2024 06:27:52 +0000 To: bob1@example.com, bob2@example.com, bob3@example.com From: Mailer <alice@example.com> Subject: Here is the subject MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 This is the test message EOF
- 如果不需要登录则可以忽略 --user 和 --login-options 参数
- --login-options 参数是用于指定登录方式的
- 从文件中读取邮件
- powershell
- Send-MailMessage
- Send-MailMessage 在文档里有明确提及不建议使用
- Send-MailMessage 不支持 465 端口的 smtps ,如果要使用 smtps 就要改用其他端口
Send-MailMessage -SmtpServer "smtp.qq.com" -Port 587 -UseSsl ` -Credential $(New-Object System.Management.Automation.PSCredential("alice@example.com", $(ConvertTo-SecureString "NvbQBTZW5kRW" -AsPlainText -Force))) ` -From "alice@example.com" ` -To 'User02 <user02@fabrikam.com>', 'User03 <user03@fabrikam.com>' ` -Subject 'Test mail' ` -Body "message"
- Net.Mail.MailMessage
- Send-MailMessage
php 发送邮件
使用原生的函数
- mail 和 mb_send_mail 函数
- 这两个函数的效果基本一样
- 有两种套路
- 发送至 smtp 服务器 (这个只能用在 windows 环境)
- 调用 sendmail 命令
- 要区分 windows 环境 linux 环境, 要关注 sendmail 这个命令
- windows 环境下在 php.ini 里配置好,就可以用 mail 发邮件了
[mail function] ; For Win32 only. ; https://php.net/smtp SMTP = localhost ; https://php.net/smtp-port smtp_port = 25 ;sendmail_from = me@example.com
- linux 环境下则确实需要 sendmail
- 这三个参数只在 windows 里生效
- SMTP
- smtp_port
- sendmail_from
- 如果 windows 里也设置了 sendmail_path 那么上面那三个也会失效
- mail 就是生成一个 邮件正文 ,然后通过标准输入,传递给 sendmail_path
- sendmail_path 默认情况下是 sendmial -t -i
- -t 的意思是 从邮件的头部信息中提取收件人地址
- -i 的意思是 忽略点号(.)单独出现在一行上的情况,这通常表示邮件的结束
- sendmail_path 除了 sendmail 之外也可以设置其他 mta
- sendmail_path 默认情况下是 sendmial -t -i
- 示例代码
// 设置收件人地址 $to = 'asd <asd@123.com>, asd2 <asd2@123.com>'; // 设置邮件主题 $subject = 'subject'; // 设置邮件正文 $message = '<p>message</p>'; // 设置邮件头部信息,包括发件人地址和回复地址 $additional_headers = [ 'From' => 'Mailer <1643253513@qq.com>', 'Content-Type' => 'text/html; charset=utf-8', 'Reply-To' => 'bing@example.com', ]; $additional_params = ''; // 调用 mail 函数发送邮件 if (mail($to, $subject, $message, $headers, $additional_params)) { echo "邮件发送成功"; } else { echo "邮件发送失败"; } // 理论上是可以在 message 里塞附件的, // 一些版本的 sendmail 可以在命令行里设置 smtp 地址和账号密码,这时就可以通过 additional_params 变量设置 // mail 函数里的 additional_params 参数可以设置 mat 的更多的命令行参数, // 其实就类似于这样 // echo "邮件正文" | sendmail -t -i additional_params
- 个人感觉 php 原生的 mail 函数并不好用,至少没有区分 信封 和 消息
使用第三方库
- https://github.com/PHPMailer/PHPMailer
//Import PHPMailer classes into the global namespace //These must be at the top of your script, not inside a function use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\SMTP; use PHPMailer\PHPMailer\Exception; //Load Composer's autoloader require 'vendor/autoload.php'; //Create an instance; passing `true` enables exceptions $mail = new PHPMailer(true); try { //Server settings $mail->SMTPDebug = SMTP::DEBUG_LOWLEVEL; //Enable verbose debug output $mail->isSMTP(); //Send using SMTP $mail->Host = 'smtp.qq.com'; //Set the SMTP server to send through $mail->SMTPAuth = true; //Enable SMTP authentication $mail->Username = 'joe@example.net'; //SMTP username $mail->Password = 'matthew'; //SMTP password $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; //Enable implicit TLS encryption $mail->Port = 465; //TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS` //Recipients $mail->setFrom('joe@example.net', 'Mailer'); // $mail->addAddress('joe@example.net', 'Joe User'); //Add a recipient $mail->addAddress('info@example.com'); //Name is optional // $mail->addReplyTo('info@example.com', 'Information'); // $mail->addCC('cc@example.com'); // $mail->addBCC('bcc@example.com'); //Attachments // $mail->addAttachment('/var/tmp/file.tar.gz'); //Add attachments // $mail->addAttachment('/tmp/image.jpg', 'new.jpg'); //Optional name //Content $mail->isHTML(true); //Set email format to HTML $mail->Subject = 'Here is the subject'; $mail->Body = 'This is the HTML message body <b>in bold!</b>'; $mail->AltBody = 'This is the body in plain text for non-HTML mail clients'; $mail->send(); echo 'Message has been sent'; } catch (Exception $e) { echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}"; }
- https://github.com/symfony/mailer
- https://github.com/laminas/laminas-mail
require './vendor/autoload.php'; use Laminas\Mail\Message; $message = new Message(); $message->addFrom('matthew@example.org', 'Matthew Somelli'); $message->addTo('foobar@example.com', 'foobar'); $message->addTo('foobar2@example.com', 'foobar2'); $message->addCc('ralph@example.org', 'ralph'); $message->addCc('ralph2@example.org', 'ralph2'); $message->addBcc('enrico@example.org', 'enrico'); $message->addBcc('enrico2@example.org', 'enrico2'); $message->addReplyTo('matthew@example.com', 'Matthew'); $message->setSender('matthew@example.org', 'Matthew Sommeli'); $message->setSubject('Sending an email from Laminas\Mail!'); // $message->setEncoding('UTF-8'); $message->setBody('This is the message body.'); // echo $message->toString(); // 这样可以生成一个 IMF 字符串, $transport = new \Laminas\Mail\Transport\Smtp(); $options = new \Laminas\Mail\Transport\SmtpOptions([ 'name' => 'localhost', 'host' => '127.0.0.1', // 'connection_class' => 'login', 'connection_class' => 'plain', 'connection_config' => [ 'username' => 'user', 'password' => 'pass', ], ]); $transport->setOptions($options); $transport->send($message);
- https://github.com/zetacomponents/Mail