2017年2月14日 星期二

在樹莓派上利用 sSMTP 傳送郵件的方法

前陣子在樹莓派安裝好 PHP+MySQL 網頁伺服器後, 發現使用 mail() 函數傳送郵件的 PHP 程式無法執行, 雖然之前處理 Crontab 無法執行時因為誤解 MTA 錯誤訊息而安裝了 POSTFIX 郵件伺服器, 但仍然無法執行 mail() 函式送出郵件 (設定問題?).

最近讀到下面這篇文章才了解, 事實上我的樹莓派不需要安裝 Postfix 那樣的郵件伺服器, 因為我不需要利用樹莓派接收郵件, 我只需要將樹莓派收集到的資訊, 例如物聯網智能家居之溫濕度/門禁/瓦斯/監視攝影機移動偵測結果,  網路爬蟲擷取分析結果, 每周或每月之系統運行摘要報告等等, 傳送到外面的郵件伺服器如 Gmail, 由我們的 Gmail 帳號代為傳送到指定的 Email 位址即可, 參考 :

# Sending emails from the Raspberry Pi

"This (sending emails from the Raspberry Pi to arbitrary recipients) is not the same as having a real MTA running on the Pi (like Sendmail, Postfix, Exim, QMail, etc.), which can also receive and store emails. .... In most cases this is enough, as people tend to use GMail, Yahoo! Mail and other major email service providers and they store their emails on the servers of these providers."

以下是我根據這篇文章, 安裝 sSMTP 程式, 然後透過 PHP 程式傳送郵件到指定的 Email 的實驗記錄. 作者建議使用 Gmail 郵件伺服器來代為傳送 Email, 因為許多郵件伺服器會將來自非固定 IP 位址的 Email 判定為垃圾郵件, 而 Gmail 郵件伺服器不會, 因此我們要準備一個 Gmail 帳號. 參考 :

"Many email servers today have very strict rules for accepting emails. For example if the email is not coming from a machine with a static IP address, they might classify the email as SPAM. We don’t want that to happen with the emails sent from the Raspberry Pi, so we are going to send the emails to a Goggle server, which will send them forward to the real recipients."

sSMTP 是啥呢? 原來它是用來傳送郵件到 SMTP 伺服器的一個簡單 MTA (Mail Transfer Agent), 但是它沒有常駐在記憶體中的 Daemon 程式, 而且沒有接收郵件的功能, 參考 :

# Dabian Wiki : sSMTP - Simple SMTP

程序如下 :

1. 更新已安裝的應用程式列表

$ sudo  sudo apt-get update

2. 安裝 ssmtp 與 mailutils 套件

$ sudo apt-get install ssmtp
$ sudo apt-get install mailutils

pi@raspberrypi:~ $ sudo apt-get install ssmtp
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following NEW packages will be installed:
  ssmtp
0 upgraded, 1 newly installed, 0 to remove and 24 not upgraded.
Need to get 54.2 kB of archives.
After this operation, 1,024 B of additional disk space will be used.
Get:1 http://mirrordirector.raspbian.org/raspbian/ jessie/main ssmtp armhf 2.64-8 [54.2 kB]
Fetched 54.2 kB in 2s (20.7 kB/s)
Preconfiguring packages ...
Selecting previously unselected package ssmtp.
(Reading database ... 129331 files and directories currently installed.)
Preparing to unpack .../ssmtp_2.64-8_armhf.deb ...
Unpacking ssmtp (2.64-8) ...
Processing triggers for man-db (2.7.0.2-5) ...
Setting up ssmtp (2.64-8) ...
pi@raspberrypi:~ $ sudo apt-get install mailutils
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following extra packages will be installed:
  guile-2.0-libs libgc1c2 libgsasl7 libkyotocabinet16 libmailutils4 libntlm0 libunistring0 mailutils-common
Suggested packages:
  mailutils-mh mailutils-doc
The following NEW packages will be installed:
  guile-2.0-libs libgc1c2 libgsasl7 libkyotocabinet16 libmailutils4 libntlm0 libunistring0 mailutils mailutils-common
0 upgraded, 9 newly installed, 0 to remove and 24 not upgraded.
Need to get 4,911 kB of archives.
After this operation, 19.1 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://mirrordirector.raspbian.org/raspbian/ jessie/main libgc1c2 armhf 1:7.2d-6.4 [122 kB]
Get:2 http://mirrordirector.raspbian.org/raspbian/ jessie/main libunistring0 armhf 0.9.3-5.2 [253 kB]
Get:3 http://mirrordirector.raspbian.org/raspbian/ jessie/main guile-2.0-libs armhf 2.0.11+1-9 [2,162 kB]
Get:4 http://mirrordirector.raspbian.org/raspbian/ jessie/main libkyotocabinet16 armhf 1.2.76-4 [289 kB]
Get:5 http://mirrordirector.raspbian.org/raspbian/ jessie/main mailutils-common all 1:2.99.98-2 [599 kB]
Get:6 http://mirrordirector.raspbian.org/raspbian/ jessie/main libntlm0 armhf 1.4-3 [19.7 kB]
Get:7 http://mirrordirector.raspbian.org/raspbian/ jessie/main libgsasl7 armhf 1.8.0-6 [193 kB]
Get:8 http://mirrordirector.raspbian.org/raspbian/ jessie/main libmailutils4 armhf 1:2.99.98-2 [701 kB]
Get:9 http://mirrordirector.raspbian.org/raspbian/ jessie/main mailutils armhf 1:2.99.98-2 [572 kB]
Fetched 4,911 kB in 24s (198 kB/s)
Selecting previously unselected package libgc1c2:armhf.
(Reading database ... 129353 files and directories currently installed.)
Preparing to unpack .../libgc1c2_1%3a7.2d-6.4_armhf.deb ...
Unpacking libgc1c2:armhf (1:7.2d-6.4) ...
Selecting previously unselected package libunistring0:armhf.
Preparing to unpack .../libunistring0_0.9.3-5.2_armhf.deb ...
Unpacking libunistring0:armhf (0.9.3-5.2) ...
Selecting previously unselected package guile-2.0-libs:armhf.
Preparing to unpack .../guile-2.0-libs_2.0.11+1-9_armhf.deb ...
Unpacking guile-2.0-libs:armhf (2.0.11+1-9) ...
Selecting previously unselected package libkyotocabinet16:armhf.
Preparing to unpack .../libkyotocabinet16_1.2.76-4_armhf.deb ...
Unpacking libkyotocabinet16:armhf (1.2.76-4) ...
Selecting previously unselected package mailutils-common.
Preparing to unpack .../mailutils-common_1%3a2.99.98-2_all.deb ...
Unpacking mailutils-common (1:2.99.98-2) ...
Selecting previously unselected package libntlm0:armhf.
Preparing to unpack .../libntlm0_1.4-3_armhf.deb ...
Unpacking libntlm0:armhf (1.4-3) ...
Selecting previously unselected package libgsasl7.
Preparing to unpack .../libgsasl7_1.8.0-6_armhf.deb ...
Unpacking libgsasl7 (1.8.0-6) ...
Selecting previously unselected package libmailutils4:armhf.
Preparing to unpack .../libmailutils4_1%3a2.99.98-2_armhf.deb ...
Unpacking libmailutils4:armhf (1:2.99.98-2) ...
Selecting previously unselected package mailutils.
Preparing to unpack .../mailutils_1%3a2.99.98-2_armhf.deb ...
Unpacking mailutils (1:2.99.98-2) ...
Processing triggers for man-db (2.7.0.2-5) ...
Setting up libgc1c2:armhf (1:7.2d-6.4) ...
Setting up libunistring0:armhf (0.9.3-5.2) ...
Setting up guile-2.0-libs:armhf (2.0.11+1-9) ...
Setting up libkyotocabinet16:armhf (1.2.76-4) ...
Setting up mailutils-common (1:2.99.98-2) ...
Setting up libntlm0:armhf (1.4-3) ...
Setting up libgsasl7 (1.8.0-6) ...
Setting up libmailutils4:armhf (1:2.99.98-2) ...
Setting up mailutils (1:2.99.98-2) ...
update-alternatives: using /usr/bin/frm.mailutils to provide /usr/bin/frm (frm) in auto mode
update-alternatives: using /usr/bin/from.mailutils to provide /usr/bin/from (from) in auto mode
update-alternatives: using /usr/bin/messages.mailutils to provide /usr/bin/messages (messages) in auto mode
update-alternatives: using /usr/bin/movemail.mailutils to provide /usr/bin/movemail (movemail) in auto mode
update-alternatives: using /usr/bin/readmsg.mailutils to provide /usr/bin/readmsg (readmsg) in auto mode
update-alternatives: using /usr/bin/dotlock.mailutils to provide /usr/bin/dotlock (dotlock) in auto mode
update-alternatives: using /usr/bin/mail.mailutils to provide /usr/bin/mailx (mailx) in auto mode
Processing triggers for libc-bin (2.19-18+deb8u7) ...

3. 編輯 ssmtp.conf 設定檔

此檔案位於 /etc/ssmtp 目錄下 :

$ sudo nano /etc/ssmtp/ssmtp.conf

預設的 ssmtp.conf 內容如下 :

#
# Config file for sSMTP sendmail
#
# The person who gets all mail for userids < 1000
# Make this empty to disable rewriting.
root=postmaster

# The place where the mail goes. The actual machine name is required no
# MX records are consulted. Commonly mailhosts are named mail.domain.com
mailhub=mail

# Where will the mail seem to come from?
#rewriteDomain=

# The full hostname
hostname=raspberrypi

# Are users allowed to set their own From: address?
# YES - Allow the user to specify their own From: address
# NO - Use the system generated From: address
#FromLineOverride=YES

這裡面共有五個參數, 其中 root, mailhub, hostname 為必要參數, 其他為可有可無的參數. 要用 Gmail 傳送檔案還必須加入 Gmail 帳號 AuthUser, 密碼 AuthPass, 與 UseSTARTTLS 這三個參數, 一共是六個必要參數 :

root=postmaster
mailhub=smtp.gmail.com:587
hostname=raspberrypi
AuthUser=MyGMailUserName@gmail.com
AuthPass=MyGMailPassword  
UseSTARTTLS=YES

其中黃色部分要填入自己的 Gmail 帳密. hostname 就是樹莓派的主機名稱, 這可以在 /etc/hosts 檔案中找到, 參考 :

# How to Change Your Raspberry Pi (or Other Linux Device’s) Hostname

如下 127.0.0.1 所對應的 host 名稱就是 raspberry :

pi@raspberrypi:~ $ sudo cat /etc/hosts
127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
127.0.1.1       raspberrypi

注意, 如果 Gmail 帳號有設兩段式認證, 則上面的 AuthPass 填入 Gmail 密碼是無法成功通過帳戶認證的, 必須進入我的帳戶中為 sSMTP 取得應用程式密碼才能登入 Gmail : 

首先在 Google 首頁右上方點一下自己的人頭, 按 "我的帳戶" :


在 "登入和安全性" 欄中點選 "登入 Google" :
然後在 "密碼和帳戶登入方式" 欄中按 "應用程式密碼" :


這時會再跳出 Google 登入畫面再次驗證身分, 輸入密碼後就會回到如下畫面, 點選最底下的 "選取裝置" :


因為這裡沒有樹莓派, 故選取 "其他 (自訂名稱)" 輸入 "Raspberry Pi" :


點選 "選取應用程式", 同樣選擇 "其他 (自訂名稱)" 輸入 "SSMTP" :


再按 "產生" 鈕就會得到給 SSMTP 的專用登入密碼, 將其複製到 /etc/ssmtp 裡的 AuthPass 參數即可 :

這個應用程式密碼要抄錄下來, 因為離開此頁就無法再查詢, 必須刪除重新產生一組. 此密碼可以用在其他任何一台樹莓派中, 即使 Gmail 帳號有申請兩段式認證都沒問題, Gmail 伺服器只要看到應用程式傳送此組密碼就會放行. 但由於這個密碼是以明碼形式寫在 /etc/ssmtp 檔中, 因此必須確認其存取權限是否為 640 以策安全, 否則以 chmod 更改 :

chmod 640 /etc/ssmtp/ssmtp.conf

pi@raspberrypi:~ $ sudo ls -l /etc/ssmtp/ssmtp.conf
-rw-r--r-- 1 root root 671 Feb 26 12:04 /etc/ssmtp/ssmtp.conf
pi@raspberrypi:~ $ sudo chmod 640 /etc/ssmtp/ssmtp.conf
pi@raspberrypi:~ $ sudo ls -l /etc/ssmtp/ssmtp.conf
-rw-r----- 1 root root 671 Feb 26 12:04 /etc/ssmtp/ssmtp.conf

這樣就可以用 echo 指令測試看看是否透過 Gmail 傳送郵件了 :

pi@raspberrypi:~ $ echo "Test text" | mail -s "Test Mail" tony@msa.hinet.net

果然可以順利收到來自 Gmail 的郵件 :


最後來測試看看如何透過 PHP 來傳送郵件, 我參考 PHP 書籍改編了一個 formatted_mail() 函式來傳送郵件 :

/*-----------------------------------------------------------------------------
formatted_mail($subject, $message, $address, $content_type)
功能 :
  此函數根據傳入之 MySQL 伺服器位址, 帳號, 密碼建立資料庫連線並傳回連線識別字.
參數 :                                
  $subject      : 郵件主旨字串
  $message      : 郵件內容字串
  $address      : 郵件位址之關聯陣列 :
                  $address['to']      : 收件者地址                    
                  $address['from']    : 寄件者地址                      
                  $address['replyto'] : 回覆地址              
                  $address['cc']      : 副本收件者地址      
                  $address['bcc']     : 密件副本收件者地址
  $content_type : 定義郵件內容之 MIME 類型.
                  "Text/plain" : 純文字            
                  "Text/html"  : HTML 格式
傳回值 :
  成功傳回 TRUE, 失敗傳回 FALSE
範例 :
  formatted_mail($subject, $message, $address, $content_type)
-----------------------------------------------------------------------------*/
function formatted_mail($subject, $message, $address, $content_type) {
  if (!isset($address['cc'])) {$address['cc']="";} //設定副本收件人預設值
  if (!isset($address['bcc'])) {$address['bcc']="";} //設定密件副本收件人預設值
  //若沒有指定回覆位址, 預設為寄件人位址 (這很重要, 因為郵件無法遞送時, 也是
  //通知 "Reply-To:" 位址的, 若沒有設定就會變成通知郵件伺服器管理員, 寄件人
  //不知道郵件沒有送達, 此處未指定時, 預設值為寄件人位址.
  if (!isset($address['replyto'])) {$address['replyto']=$address['from'];}  
  //設定郵件標頭
  $headers="From: ".utf8_b64($address['from'])."\r\n"; //設定標頭:寄件人
  $headers .= "Return-Path: ".$address['from']."\r\n"; //設定標頭:回傳路徑
  $headers .= "Reply-To: ".$address['replyto']."\r\n"; //設定標頭:回覆位址
  if (strlen($address['cc'])< 0 ) { //設定副本收件人
      $headers = $headers . "Cc: ".$address['cc']."\r\n";
      }
  if (strlen($address['bcc'])< 0 ) { //設定密件副本收件人
      $headers = $headers . "Bcc: ".$address['bcc']."\r\n";
      }
  $headers=$headers."Content-Type: ".$content_type."\r\n"; //標頭加上內容類型
  $subject=utf8_b64($subject); //轉換為 base64 之 utf-8, 避免中文引起之亂碼
  //$subject=mb_encode_mimeheader($subject,'UTF-8'); //無效
  $result=mail($address['to'], $subject, $message, $headers); //傳送郵件  
  return $result;
  }
/*-----------------------------------------------------------------------------
utf8_b64($nonascii)
功能 :
  此函數呼叫 base64_encode() 將非 ASCII 編碼之字串 (例如中文字) 加以編碼, 以免
  郵件主旨變成亂碼. 關於郵件主旨亂碼問題 :
  郵件內文可以透過標頭中的 "Content-Type:text/plain;charset=utf-8\r\n" 設定,
  但是主旨 Subject 與寄件人, 收件人, 副本收文者等均非內文, 但卻有可能出現中文
  (收信者可能使用名稱代號), 在 PHP 傳送郵件時會出現亂碼情形. 在 RFC2047 有提供
  關於在標頭中傳送非 ASCII 碼的修正方式, 以使用 utf-8 傳送為例, 應將原主旨用
  base64_encode() 函式轉換後以 "=?utf-8?b?" 與 "?=" 括起來. 修正格式以 "=?"
  起始, 以 "?=" 結束. 第一個參數指定編碼字元集, 第二個參數為編碼格式, b 代表
  Base64 (以英文字母, 數字, 以及 % 等 64 個字元組成).
參數 :                                
  $nonascii : 含有非 ASCII 碼之字串 (如中文)
傳回值 :
  傳回 TRUE, 失敗傳回 FALSE
範例 :
  $from=utf8_b64($_POST["from"]);
  $subject=utf8_b64($_POST["subject"]);
-----------------------------------------------------------------------------*/
function utf8_b64($nonascii) {  
  return $utf8b64="=?utf-8?b?".base64_encode($nonascii)."?=";
  }
?>

PHP 測試程式如下 :

$subject="Test Mail";
$message="Test Text";
$address["to"]="tony@msa.hinet.net";          //收件人
$address["cc"]="amy@gmail.com";              //副本
$address["bcc"]="kelly@yahoo.com";          //副本密件
$address["replyto"]="peter@raspberry.org"; //回信到
$address["from"]="許功蓋";                          //寄件人 (測試中文亂碼用)
$content_type="text/html;charset=UTF-8";
$result=formatted_mail($subject,$message,$address,$content_type);
if ($result==TRUE) {
    echo "Your mail has been sent.<br>";
    foreach ($address as $key => $value) {
    echo "$key: $value<br>";
    } //end of foreach
    echo "Subject : $subject<br>";
    echo "Message : $message<br>";
    } //end of if
else {echo("Mail-sending fails.");}

參考 :

ssmtp to send emails
how to set sender information in ssmtp mail client
# ssmtp.conf FromLineOverride=YES working with google apps
Sending email with PHP from an SMTP server
# Raspberry Pi Email Server Part 1: Postfix

2017-02-15 補充 :

安裝 sSMTP 後發現, 我的 Gmail 收到一大堆從樹莓派送出的郵件, 每五分鐘就一封, 可見每執行一個 crontab 就送出一封, 這是不需要的, 只要執行特定 PHP 程式時才需要傳送 Email, 如何關掉 Crontab 傳送 Emailm 的功能呢? 參考下列這篇 :

How to disable emails from Crontab?

原來只要在每個 Crontab 指令後面加上 ">/dev/null 2>&1" 就可以了.

20170216 補充 :

今天檢查 Gmail, 發現 Crontab 每天會傳送一個系統報告 :








8 則留言 :

陳則 提到...

你好
感謝您提供的方法
參考您的作法後 發現發信者沒改變 一直維持著www-data
請問該調整什麼呢 謝謝!

小狐狸事務所 提到...

陳則您好, /etc/ssmtp/ssmtp.conf 裡面的 AuthUser=MyGMailUserName@gmail.com 要改成您的 Gmail 帳號喔! 另外, Google 兩階段認證取得授權做法已經改變了, 參考 :
https://yhhuang1966.blogspot.com/2020/12/google-gmail-smtp.html

TTM 提到...

您好!
我在發送訊息的時候出現
mail: cannot send message: Process exited with a non-zero status
是甚麼問題呢?

TTM 提到...
作者已經移除這則留言。
TTM 提到...

https://upload.cc/i1/2022/04/07/JU7gGw.png
以上是不是可以順利寄出媽?不過我的gmail卻收不到信請問是哪裡有問題呢?

小狐狸事務所 提到...

嗨, TTM, 有可能是 gmail 兩階段認證問題, 我樹莓派用上面程式每小時寄信給我都有收到喔!

TTM 提到...

不過我沒有設定兩段驗證耶~還是說一定要開啟呢?

小狐狸事務所 提到...

應該不用, 我找時間跟人家借沒有兩階段的 gmail 帳號驗證看看