2019年6月14日星期五

使用 AppleScript 批量发送带附件的邮件

最近某人要代表学校批量发送邮件欢迎新生,而且有几个特殊的要求:

  1. 邮件必须每个人独立发一封,因为给每个人的邮件要正确的称呼这个人的名字。因此邮件必须批量发送,而不能够以 bcc 的形式一封邮件发给所有人。
  2. 邮件需要带上附件。
  3. 邮件没办法提供「退订」链接。这导致我们不能够使用 MailChimp,因为 MailChimp 根据法律要求邮件列表必须提供退订链接。

为此我想到了用 AppleScript 来控制 Mac 上的 Mail 来批量发送邮件。搜索一番后,写出如下的 AppleScript 来:

在这个 AppleScript 里,我通过 theRecipients 提供一个姓名及邮件的列表,然后打开系统对话框选择一个用作附件的邮件,接着利用循环逐一发送邮件。代码里面最 tricky 的一行是 delay 1,如果没有这一行代码附件会添加失败,但邮件文本会被发送出去。我也无法解释为什么会这样子,使用 delay 1 是我从网上搜索回来的解决方案,如果你知道为什么要这样做,请在评论告诉我。

最后,现在 AppleScript 的文档实在难找。例如说我想找 Mail 支持的所有 AppleScript 操作现在已经找不到了,Apple 网站上一些原有的 AppleScript 文档链接已经失效。现在能做的也就是看着别人过去留下的 AppleScript 自己慢慢摸索。如果你有更好的 AppleScript 文档,请在评论分享一下。

2019年6月5日星期三

Patreon 付费读者交流群

如果你是我的 Patreon 付费读者,你现在可以加入付费读者专属的 Discord 交流群。加入办法很简单:首先,如果你还没有 Discord 帐号的话可以先注册一个;然后,你可以在 Patreon 的 App 设置页关联你的 Discord 帐号。接下来 Patreon 会自动利用 Discord bot 把你添加到我的 Discord 服务器及对应的付费读者专属群。(这是此功能的 Patreon 官方帮助。)

如果你还不是我的付费读者,你可以使用邀请连接加入我的 Discord 服务器。相对于非付费读者来说,付费读者享受以下两项福利:

  1. 付费读者能够加入到我服务器上的 #early-reader 群,我会在上面跟大家讨论任何跟我文章相关的问题。
  2. 付费读者拥有高亮的用户名,在我服务器上的开放群拥有更高的可识别度。

如果你现在还不是我的付费读者,欢迎通过我的 Patreon 成为付费读者。现在付费读者有两档:

  • Early Reader: 每个月 $1,能够比其它人提前一个星期读到我发布的文章。如果你喜欢我写的文章,这是支持我写作的最佳途径。
  • Career Coaching: 每个月 $100,我每两周跟你面谈 30 分钟(面对面或视频),对你的职业成长进行辅导。如果你是从事互联网和科技相关的工作,或者你计划毕业后从事相关工作,我们可以试着聊一下看看我对你的职业成长是否能够提供帮助。

最后,如果你是非美国用户,觉得 Patreon 付费不方便(例如说不接受你的信用卡或你没有美元信用卡),请在评论留言告诉我,我会想办法提供其它付费方式。感谢大家对我一直以来的支持!

2019年4月19日星期五

如何重置 macOS 的蓝牙模块

问题现象

我买了新的 Bose QC35 II 主动降噪耳机,买的原因是我老的 QC25 不支持蓝牙且磨损到一定程度了。新耳机到手后,跟 iPhone 配对没问题,但跟我的一台 MacBook Pro 就是无论如何也配对不上。通过跟另外一台 MacBook Pro 配对,证明问题发生在这台 MacBook Pro,而 Bose QC35 本身没问题。

无法配对的具体现象是,在 macOS 设置的蓝牙面板中无论如何也搜索不到 Bose QC35。(而且也搜索不到周边的其他蓝牙设备。但 Apple 自己的蓝牙键盘鼠标之前能正常配对,因为那是通过 Lightning 线配对的。)我尝试过重启 macOS,但依然搜索不到任何蓝牙设备。

解决方案

解决方案就是重置 macOS 的蓝牙。我搜索后发现,老的 macOS(准确来说还是 Mac OS X)需要删除 Bluetooth.plist 来重置,新的 macOS 已经不用那么麻烦。同时按住 Shift 和 Option,然后点击菜单栏上的蓝牙图标,这时候下拉菜单就会显示更多信息,包括一个「Debug」子菜单。打开「Debug」子菜单,然后选择「Reset the Bluetooth module」,并且确认真的要重置,最后重启 macOS,问题就解决了。之后我能在蓝牙面板中搜索到 Bose QC35,配对后播放音乐没有任何问题。

2019年2月26日星期二

把我的个人网站推倒重来(Part 8 - Sitemap)

为了让 Google 更好地发现和索引我的网站,我会使用 Google Search Console 提交 Sitemap。Sitemap 本质上是一个 XML 文件,描述网站包含的网页信息,帮助 Google 了解网站上都有哪些网页,以及这些网页的重要程度和更新频率。(Google 会参考这些信息,但我们不能通过 Sitemap 直接控制 Google 爬虫或排名。)我上一版的个人网站就包含了 Sitemap,所以重新新版本时也会把 Sitemap 加上。

上一个版本的 Sitemap 是我手工维护的 XML 文件,既然新版本的网站使用 Harp 进行编译,我希望 Sitemap 的 XML 文件也是编译时自动生成的。Harp 要使用模板生成 XML 文件很简单,把文件的后缀写成 .jade.xml 就可以了,Harp 从文件名可以推断出这个文件需要使用 Jade 模板来编译,结果保存为 XML 文件。我希望这个文件尽可能不需要手工维护,最好自己根据语言和页面的组合生成所有有效页面的链接。

每一个有效页面背后都有一个同路径的 .jade 文件,例如说 /en/resume 背后存在对应的 /en/resume.jade,这个文件会生成 /en/resume.html,然后 Netlify 会美化 URL 删掉不必要的 .html 后缀。如果我需要找出所有的有效页面,其中一个方法就是遍历项目目录,找出所有这些 .jade 文件,然后把它们的路径映射过去。因为遍历所有目录需要用到递归,我就写了一个 mixin 来做这个事情,原因是 Jade 里面的 mixin 类似于函数,可以自己调用自己从而实现递归。

mixin tree(current, path)
  for value, key in current
    if key === '_contents'
      for file in value
        if /\.html$/.test(file) && !/^\d{3}\.html$/.test(file)
          url
            loc
              | https://catchen.me#{path}#{file.replace(/(index)?\.html$/, '')}
            changefreq
              | hourly
            priority
              if path === '/' && file === 'index.html'
                | 1.0
              else
                | #{public._data[file.replace(/\.html$/, '')].priority}
    else if !/^[._]/.test(key)
      mixin tree(value, path + key + '/')

这个 mixin 接收两个参数,第一个是代表当前目录的对象,第二个是当前目录所在路径。在递归的入口,我会调用 mixin tree(public, '/'),把代表根目录的 public 对象和对应的路径字符串 '/' 传进去,然后这个 mixin 就会遍历所有子目录把所有编译后的 .html 文件找出来。它找到的每一个 .html 文件都会被添加到 Sitemap 里面。

这个 mixin 在把 .html 文件映射为 URL 时,它还做了以下的特殊处理:

  1. 删除 .html 后缀,因为 Netlify 在美化 URL 时也会进行这个操作。如果我们在浏览器中打开 https://catchen.me/zh/resume.html,Netlify 会返回 301 重定向到 https://catchen.me/zh/resume,保证用户看到的是不包含 .html 的路径。
  2. 删除 index 默认文件名,因为 https://catchen.me/zh/index.htmlhttps://catchen.me/zh/ 等价,同样会被 URL 美化删除。
  3. 过滤掉 404.html 等状态码特殊页面,因为这个页面是专门给 Netlify 返回对应状态码时使用的,不应该被索引。

最终的 sitemap.jade.xml 其实并不复杂,记得加上 doctype xmlurlset(xmlns="http://www.sitemaps.org/schemas/sitemap/0.9") 这类必要的元素就行了。编译后立即可以发布到 Netlify,然后在 Google Search Console 进行手工添加 Sitemap。数天后 Google Search Console 就会显示这个 Sitemap 已经被抓取了,然后还能看到 Sitemap 中页面的抓取情况。

这有可能是这个系列的最后一篇文章了,将来如果我对我的个人网站继续有更新,我会继续写这个系列。如果你对我的博客感兴趣的话,欢迎通过邮件RSS/Atom 进行订阅。如果你对这个话题有什么问题或者观点的话,欢迎对本文发表评论。

2019年1月17日星期四

把我的个人网站推倒重来(Part 7 - Google Analytics & Facebook Pixel)

网站上线之后,我自然关心访客的数量和来源,于是我决定加上 Google Analytics。同时纯粹出于好奇,我把 Facebook Pixel 也加上了。

Google Analytics 和 Facebook Pixel 都需要插入 JavaScript 到每一个页面上,因此把代码加到 _layout.jade 是最合适的,因为这是所有页面共享的模板。

Google Analytics

在 Google Analytics 创建好「property」后,复制 Google Analytics 生成的代码到 _layout.jade 其实就完事了。(Google 把独立的每一个网站和 app 叫做「property」,这样使得一个 Google 帐号可以管理多个网站和 app。)

因为我想知道我的网站是否有 JavaScript 出错,所以我特意通过 window.onerrorwindow.onunhandledrejection 事件把错误信息上报到 Google Analytics。如果 Google Analytics 在创建 property 时选择的是网站而不是 app,默认是不会有报告显示 exception 事件的。这时候我们需要手工在 Google Analytics 添加一份自定义报告,然后选择显示 exception 事件,并把 exception message 展示出来。这样我就能看到是否存在 JavaScript 错误,以及具体在哪个页面发生什么错误了。

Facebook Pixel

Facebook Pixel 跟 Google Analytics 类似,在 Facebook 上配置后然后复制粘贴代码就可以了。需要注意的是,一个 Facebook 广告帐号只能创建一个 Pixel,不像 Google Analytics 那样可以创建多个 property。如果需要跟踪多个网站,那就需要创建多个广告帐号,或者一个 Pixel 用不同的事件来跟踪不同的网站。

在配置好 Facebook Pixel 并运行一段事件后,我发现 Facebook Pixel 并不能提供 Google Analytics 那样复杂的分析功能,更多是为了跟踪广告效果和生成广告受众群体。例如说,如果我在我的博客上使用 Facebook Pixel,然后我就能生成一个访问我博客的受众群体,然后在 Facebook 上面向这些受众投放广告让他们来赞我的专页。除此之外,我没有去深入研究 Facebook Pixel 还能做什么别的事情。(如果你知道 Facebook Pixel 还能做什么别的事情,欢迎在评论区讨论。)

搞掂 Google Analytics 和 Facebook Pixel 后,我还想做的是支持 Sitemaps 好让 Google 抓取所有页面。如果你对此感兴趣的话,欢迎通过邮件RSS/Atom 订阅我的博客,关注接下来的文章。