从Bash shell脚本发送邮件

39
我正在编写一个 Bash shell 脚本,用于在 Mac 上通过打开一个自动化应用程序发送电子邮件通知,该应用程序使用 Mail.app 中的默认邮件帐户发送电子邮件。自动化应用程序还会附加脚本编写的文本文件。这种解决方案存在以下问题:
  1. 在发送时在 GUI 中可见
  2. 如果 Mail 不是当前应用程序,则会窃取焦点
  3. 它依赖于将来 Mail.app 的帐户设置有效性
我想要绕过这些缺点,应该从脚本直接发送邮件,通过在脚本中输入 SMTP 设置、发送地址等来实现。但问题在于,我想在多台计算机(10.5 和 10.6)上部署此脚本,而不需要在计算机上启用 Postfix。是否可能在脚本中执行此操作,以便在基本的 Mac OS X 10.5 和 10.6 安装上运行?
更新:我发现了 Sendmail 的 -bs 选项,这似乎是我需要的,但我不知道如何指定设置。
另外,为了澄清一下,我想指定 SMTP 设置的原因是,从端口 25 发送的 localhost 邮件会被大多数企业防火墙阻止,但如果我指定服务器和备用端口,则不会遇到这个问题。

2
我无法提供完整的解决方案(真的不太确定怎么做),但你应该查看一下sendmail程序。 - Jeffrey Aylesworth
1
这个问题应该发布在superuser.com还是serverfault.com? - t0mm13b
@tommieb75:为什么?这绝对与编程有关。 - Boldewyn
12个回答

56

考虑使用Python脚本替代Bash脚本,因为Mac OS X 包含Python。我没有测试发送部分,但它遵循标准示例

Python脚本

# Settings

SMTP_SERVER = 'mail.myisp.com'
SMTP_PORT = 25
SMTP_USERNAME = 'myusername'
SMTP_PASSWORD = '$uper$ecret'
SMTP_FROM = 'sender@example.com'
SMTP_TO = 'recipient@example.com'

TEXT_FILENAME = '/script/output/my_attachment.txt'
MESSAGE = """This is the message
to be sent to the client.
"""

# Now construct the message
import smtplib, email
from email import encoders
import os

msg = email.MIMEMultipart.MIMEMultipart()
body = email.MIMEText.MIMEText(MESSAGE)
attachment = email.MIMEBase.MIMEBase('text', 'plain')
attachment.set_payload(open(TEXT_FILENAME).read())
attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(TEXT_FILENAME))
encoders.encode_base64(attachment)
msg.attach(body)
msg.attach(attachment)
msg.add_header('From', SMTP_FROM)
msg.add_header('To', SMTP_TO)

# Now send the message
mailer = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
# EDIT: mailer is already connected
# mailer.connect()
mailer.login(SMTP_USERNAME, SMTP_PASSWORD)
mailer.sendmail(SMTP_FROM, [SMTP_TO], msg.as_string())
mailer.close()

希望这有所帮助。


1
我刚刚测试了一下,它对我有很大帮助。起初它并没有起作用,为了让它工作,我不得不(不太明白为什么)从这行代码中删除参数 mailer = smtplib.SMTP(SMTP_SERVER, SMTP_PORT) 并在“connect”调用中传递它们。有人能确认/解释一下吗?(Python 2.7.2,OS x 10.8) - KillerX
2
@KillerX 感谢您指出这个问题。之前的使用方法是错误的。在阅读了docs之后,我发现如果在SMTP构造函数中传递了主机/端口,则会自动调用connect。第二次调用connect仅尝试重新连接到“localhost:25”,这可能是不正确的。 我已更新示例以省略对connect的调用(但将其作为注释保留)。只向connect方法提供主机/端口的方法也是有效的。感谢您的反馈。 - Jason R. Coombs
2
@BSUK:在 add_header('To', ...) 之后,使用 msg.add_header('Subject', SUBJECT),其中 SUBJECT 是一个字符串字面量或包含主题字符串的变量。 - Jason R. Coombs
1
互联网上充斥着如何使用Python发送电子邮件的陈旧示例。在Python 3.6+中,您应该使用现代的EmailMessage API,而不是传统的MIMEMultipart等混乱的方法。email模块文档中的示例是一个很好的起点。还可以参考smtplib文档了解发送部分。关于具体可行的方法(例如使用端口587的SMTPstartts,或者使用端口465的SMTP_SSL,或其他方式),取决于您尝试使用的个别服务器。 - tripleee
1
感谢 @tripleee 的提醒。我已经更新了答案,以更符合现代的期望。 - Jason R. Coombs
显示剩余5条评论

41

实际上,“mail”同样有效。

mail -s "subject line" name@address.ext < filename

只要您的机器上设置了SMTP,那么这将完美运行。我认为大多数Mac默认都有SMTP。

如果您没有SMTP,则唯一能做的就是通过Mail.app发送邮件。通过AppleScript进行邮件.app的替代方式是一种选择。当您通过AppleScript告诉Mail.app发送邮件时,您可以告诉它不弹出任何窗口...(但确实需要配置Mail.app)。

介绍如何使用AppleScript操作Mail对如何在AppleScript中处理邮件有一个很好的描述。


我目前正在使用Postfix,我的脚本包含以下行:cat /pathto/file.txt | mail -s "subject" address@example.com。我意识到我可以编辑Postfix并指定一个SMTP服务器,使用http://www.macosxhints.com/article.php?story=20081217161612647,但我有兴趣将设置放在bash脚本本身中。Applescript的想法很好。我喜欢Mail.app不会从用户那里窃取焦点(就像它在我的自动化工作流中所做的那样),但它仍然需要正确设置Mail.app。如果我无法将这些设置放入我的脚本中,Applescript可能是最佳选择。 - ridogi
1
请注意,您需要按下Control-D来发送。 - incandescentman
1
这是最简单的方法。它非常有效。比编写Python脚本要好得多;-) - Jeff Whiting
我在Mac上尝试使用mail -s "主题" name@address.ext "你好",但它一直等待着什么。我做错了什么? - Anton Tarasenko
mail本身只是连接到您的本地Sendmail / Postfix,所以这并不是对问题的真正回答。如果您已经配置了本地MTA以通过Gmail或其他方式发送邮件,则当然可以使用该服务器进行传出电子邮件提交(就像使用Mail.app或Thunderbird等一样);但是OP想要避免这种情况。 - tripleee
显示剩余4条评论

15

有一个名为Sendmail的程序。

除非像马丁的例子一样以原始SMTP形式发送,否则您可能不想使用-bs命令。 -bs用于作为守护进程运行SMTP服务器。 Sendmail会直接发送到接收邮件服务器(端口25),除非您在配置文件中覆盖它。您可以通过-C参数指定配置文件。

在配置中,您可以指定中继服务器(任何邮件服务器或在另一台机器上运行-bs的sendmail)

使用经过适当配置的中继服务器是个好主意,因为在IT管理邮件服务器时,他们会实施SPFdomain keys。 这使得您的邮件不会落入垃圾箱。

如果端口25被阻止,则只剩下两个选项。

  1. 使用公司SMTP服务器。
  2. 在公司防火墙之外的计算机上运行sendmail -bd,并侦听端口25以外的端口。

我相信您可以在命令行上添加配置参数。您需要的是SMART_HOST选项。因此,像这样调用Sendmail:sendmail -OSMART_HOST=nameofhost.com


3
为什么会有人点踩呢?sendmail -tbyron@address.com \nhellow byron\n. 对我来说完全有效!请问有什么问题吗? - Byron Whitlock
3
因为你的回答几乎没有任何努力。 - user3850
7
如果答案已经足够好,我认为没必要再花更多的精力。多并不总是更好。 - Fortega
10
投票应该反映答案的相关性,而不是投入其中的努力。 - mouviciel
我可以使用指定本地SMTP服务器的选项,但我想知道如何在Bash脚本中指定它。 - ridogi
显示剩余2条评论

10

如果您直接从客户端访问SMTP服务器,则可能是保持程序自给自足的唯一方法。

如果您确实可以直接访问SMTP服务器,则可以使用维基百科上的SMTP示例并将其转换为类似以下内容:

#!/bin/bash
telnet smtp.example.org 25 <<_EOF
HELO relay.example.org
MAIL FROM:<joe@example.org>
RCPT TO:<jane@example.org>
DATA
From: Joe <joe@example.org>
To: Jane <jane@example.org>
Subject: Hello

Hello, world!
.
QUIT
_EOF

处理错误时,我会将telnet的输出重定向到文件中,然后稍后使用grep搜索“成功消息”。我不确定消息的格式应该是什么样子的,但我可以从我的SMTP服务器的输出中看到类似于“250 2.0.0 Ok: queued as D86A226C574”的内容。这使我可以使用grep搜索"^250.*queued as"。


-1 这是一个不错的例子,但在真实环境中没什么用。首先:错误处理在哪里? - Tomas
在数据部分的标题和内容之间不应该有空行吗? - Boldewyn
3
在 StackOverflow 上的答案大多数情况下并不适用于工业级别的健壮性。作为概念的证明,这确实非常好。 - Boldewyn
从技术上讲,Telnet 是一个外部程序,因此它不是自给自足的 :) 然而,SMTP 示例加一分。 - Kimvais
@Kimvais:grep、bash等也不是;-) 另外,我找不到 telnet 不是默认安装的任何迹象。 - Martin Olsen

7

我为这个挑战做了这个。如果你删除调用“dig”获取邮件中继的部分,它就是一个100%本地的Bash脚本。

#!/bin/bash
MAIL_FROM="sfinktah@bash.spamtrak.org"
RCPT_TO="sfinktah@bash.spamtrak.org"
MESSAGE=message.txt
SMTP_PORT=25
SMTP_DOMAIN=${RCPT_TO##*@}

index=1
while read PRIORITY RELAY
do
    RELAY[$index]=$RELAY
    ((index++))
done < <( dig +short MX $SMTP_DOMAIN )

RELAY_COUNT=${#RELAY[@]}
SMTP_COMMANDS=( "HELO $HOSTNAME" "MAIL FROM: <$MAIL_FROM>" "RCPT TO: <$RCPT_TO>" "DATA" "." "QUIT" )
SMTP_REPLY=([25]=OK [50]=FAIL [51]=FAIL [52]=FAIL [53]=FAIL [54]=FAIL [55]=FAIL [45]=WAIT [35]=DATA [22]=SENT)

for (( i = 1 ; i < RELAY_COUNT ; i++ ))
do
    SMTP_HOST="${RELAY[$i]}"
    echo "Trying relay [$i]: $SMTP_HOST..."
    exec 5<>/dev/tcp/$SMTP_HOST/$SMTP_PORT
    read HELO <&5
    echo GOT: $HELO
    for COMMAND_ORDER in 0 1 2 3 4 5 6 7
    do
            OUT=${SMTP_COMMANDS[COMMAND_ORDER]}
            echo SENDING: $OUT
            echo -e "$OUT\r" >&5

            read -r REPLY <&5
            echo REPLY: $REPLY
            # CODE=($REPLY)
            CODE=${REPLY:0:2}
            ACTION=${SMTP_REPLY[CODE]}
            case $ACTION in
                    WAIT )          echo Temporarily Fail
                                                    break
                                                    ;;
                    FAIL )          echo Failed
                                                    break
                                                    ;;
                    OK )                            ;;
                    SENT )          exit 0
                                                    ;;
                    DATA )          echo Sending Message: $MESSAGE
                                                    cat $MESSAGE >&5
                                                    echo -e "\r" >&5
                                                    ;;
                    * )         echo Unknown SMTP code $CODE
                                                    exit 2
            esac
    done
done

我认为这是一个非常酷的脚本,并为自己的目的创建了一个稍微修改过的版本,曾经可以工作,但几年后,当我尝试时,它失败了。不确定bash是否已更改。Bash抱怨“WAIT”情况中的“break”仅在'for'、'while'或'until'循环中有意义。就目前而言,“break”在“do”循环内的“case”部分中。我90%确定这曾经完美地运行。 - Marnix A. van Ammers
在纯Bash中重新实现SMTP客户端通常是一项令人沮丧的工作。这在简单的情况下可能会正常工作,但当服务器不完全符合您的假设时,可能会出现严重问题。 - tripleee
用纯Bash重新实现SMTP客户端通常是一项令人沮丧的工作。在简单的情况下,这可能会正常运行,但当服务器不完全符合您的假设时,可能会出现严重问题。 - undefined
@tripleee 如果我20多年前编写的SMTP服务器没有经过任何修改仍然完美运行,我可能会感到担忧。不过,如果没有IPv4而只有IPv6,可能会导致它无法正常工作。除此之外,SMTP有一个明确定义的RFC,只要该RFC不改变,那个脚本应该能够正常运行。https://github.com/sfinktah/dummymail - Orwellophile

6

sendEmail是一个脚本,您可以使用它来发送电子邮件,包括连接到远程smtp服务器等更复杂的设置,从命令行发送。 http://caspian.dotconf.net/menu/Software/SendEmail/

在OSX上,您可以通过macports轻松安装它:http://sendemail.darwinports.com/

以下是该命令的帮助页面,请注意-s、-xu、-xp标志:

Synopsis:  sendEmail -f ADDRESS [options]

Required:
  -f ADDRESS                from (sender) email address
  * At least one recipient required via -t, -cc, or -bcc
  * Message body required via -m, STDIN, or -o message-file=FILE

Common:
  -t ADDRESS [ADDR ...]     to email address(es)
  -u SUBJECT                message subject
  -m MESSAGE                message body
  -s SERVER[:PORT]          smtp mail relay, default is localhost:25

Optional:
  -a   FILE [FILE ...]      file attachment(s)
  -cc  ADDRESS [ADDR ...]   cc  email address(es)
  -bcc ADDRESS [ADDR ...]   bcc email address(es)

Paranormal:
  -xu USERNAME              authentication user (for SMTP authentication)
  -xp PASSWORD              authentication password (for SMTP authentication)
  -l  LOGFILE               log to the specified file
  -v                        verbosity, use multiple times for greater effect
  -q                        be quiet (no stdout output)
  -o NAME=VALUE             see extended help topic "misc" for details

Help:
  --help TOPIC              The following extended help topics are available:
      addressing            explain addressing and related options
      message               explain message body input and related options
      misc                  explain -xu, -xp, and others
      networking            explain -s, etc
      output                explain logging and other output options

6

使用一行代码在Bash中发送电子邮件:

echo "your mail body" | mail -s "your subject" yourmail@yourdomain.com -a "From: sender@senderdomain.com"

6
在OS X Mavericks上出现了"send-mail: illegal option -- a"错误。 - Dima Korobskiy
1
echo "你的邮件正文" | mail -s "你的主题" -F 收件人@域名.com 对我有用。 - mossman
这主要只是对Brian Postow在2009年回答的重新表述,具有相同的根本缺陷。正如早期的评论所指出的那样,MacOS的mail不支持-a选项。(也许可以参考https://dev59.com/OnVD5IYBdhLWcg3wU5-H#48588035了解不同`mail`版本的详细说明。) - tripleee

4
这里有一个简单的Ruby脚本可以实现此功能。Ruby已经预装在你提到的Mac OS X版本中。
将所有标记为“replace”的位替换掉即可。如果失败,它会返回一个非零的退出代码和一个Ruby回溯。
require 'net/smtp'

SMTPHOST = 'replace.yoursmtpserver.example.com'
FROM = '"Your Email" <youremail@replace.example.com>'

def send(to, subject, message)
  body = <<EOF
From: #{FROM}
To: #{to}
Subject: #{subject}

#{message}
EOF
  Net::SMTP.start(SMTPHOST) do |smtp|
    smtp.send_message body, FROM, to
  end
end

send('someemail@replace.example.com', 'testing', 'This is a message!')

您可以像这样将其嵌入Bash脚本中:
ruby << EOF
 ... script here ...
EOF

如果您想了解其他发送Ruby电子邮件的方法,请参阅Stack Overflow问题如何从Ruby程序发送邮件?

您还可以使用随Mac OS X捆绑的其他语言:


3

sendmail甚至postfix可能太大了,如果你只想从你的脚本中发送几封电子邮件。

例如,如果您有一个Gmail帐户,您可以使用Google的服务器使用SMTP发送电子邮件。如果您不想使用Google的服务器,只要您有访问某些SMTP服务器的权限,就应该可以正常工作。

一个非常轻量级的程序,使这变得容易的是msmtp。他们在他们的文档中提供了配置文件示例

最简单的方法是设置一个系统范围的默认值:

account default
host smtp.gmail.com
from john.doe@gmail.com
user john.doe@gmail.com
password XXX
port 587

msmtp应该很容易安装。实际上,有一个端口可供使用,因此只需执行port install msmtp即可。

安装和配置msmtp后,您可以使用以下命令将电子邮件发送到john.doe@gmail.com

mail -s <subject> john.doe@gmail.com <<EOF
<mail text, as many lines as you want. Shell variables will be expanded>.
EOF

您可以将上述内容放在脚本中。有关详细信息,请参见man mail

3

1) 为什么不配置postfix只处理出站邮件并通过邮件网关进行中继呢?它最大的优点是已经安装在OS X客户端上。

2) 安装和配置其中一个轻量级MTA,如nullmailerssmtp(可通过MacPorts获取),仅处理出站邮件。

在这两种情况下,使用mailx(1)(或如果您想要高级一些则使用mutt)从shell脚本发送邮件。

在Server Fault上有几个问题详细讨论了这些内容。


我想让这个脚本在许多机器上轻松部署,而无需配置像 postfix 这样的其他内容,因此我希望它在脚本中是自包含的。我会研究 nullmailer 和 ssmtp。使用 OS X 自带的 sendmail 或 smtpd 是否可能实现这一点? - ridogi
额...?sendmail是postfix的一部分,而postfix是os x上的smtpd,那么你的问题是什么? - user3850
我不知道它们是后缀表达式的组成部分还是可以独立操作。 - ridogi
了解nullmailer很好,但我更喜欢postfix而不是安装任何东西来简化部署此脚本。我没有找到有关设置postfix以处理我指定的邮件服务器的出站邮件的任何说明。我假设我不能仅在我的shell脚本中指定这些设置,但这对我来说是理想的工作方式。如果不可能,那么需要修改哪些postfix配置文件? - ridogi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接