About

  • Macintosh の自作ソフトウェアを公開しています。
  • 配布しているアプリケーションは、すべてフリーウェアかつソースコードを公開しています。
  • コメント、不具合の報告、ご要望を待っています。

Donation

このサイトで配布しているソフトを常用されている方は寄付をお願いします。

2016.07.21

16:22:16Permanent Link

OSAX Delete Messages Now 1.1 : Mail.app で選択されているメッセージをすぐさま消去

Mail.app で選択されているメッセージをゴミ箱に移動すること無く、すぐさま消去します。使い方は、ただ起動するだけです。スクリプトメニューなどに登録して使ってください。

たくさんのメッセージが選択されているときは、いくらかいくらか処理に時間がかかります。終了したら通知センターを使って通知します。

自分は、もっぱら「迷惑メール」フォルダの掃除に使っています。迷惑メールは相変わらず、すごい勢いでやってきます。迷惑メールフィルターのおかげでそれらを目にすることはほとんど無いのですが、必要はメールも間違って迷惑メールと判断されてしまうことも無い訳ではなく、ときどき迷惑メールフォルダの中身を確認しています。その時、ついでに確認した迷惑メールを消去するのに便利です。

2016.07.11

22:00:27Permanent Link

ModuleLoader の仕組み

ModuleLoader は AppleScript のライブラリシステムです。OS X 10.9 (2013年) で、ようやく AppleScript にライブラリシステムが標準搭載されましたが(AppleScript Libraries)、それから先駆けて3年以上前からほぼ同じことをできる実用的なシステムを確立していました。当然、OSX 10.9 以前のバージョンでも動作します。紆余曲折ありましたが、現在の仕組みは 2010-01-12 に公開したバージョン 2.1 で確立しました。

AppleScript Libraries が登場したので、お役目ご免かといえば、そうでもないと考えています。AppleScript Libraries には真似できない幾つかのことができます。

  • ライブラリをロードするタイミングを完全に制御できます。
  • コンパイル時にすべてのライブラリを読み込んでしまうことができます。
    • ライブラリが用意されていない、他の Mac にコピーしても動作するようにすることができます。

ModuleLoader と AppleScript Libraries では、後述するように、やっていることはよく似ていると思われます。今更ですが、ModuleLoader の仕組みを解説することにも意味があるのではと考えています。

ModuleLoader の基本構文

まずは、ModuleLoader を使ってのライブラリを読み込む基本的な構文です。

property Lib1 : module
property _ : boot (module loader) for me

Lib1's do_something()

property で読み込むライブラリを指定します。property の 値として module コマンドを設定すると、その property に ライブラリを読み込むんだという印 (module specifier) がつきます。ライブラリの名称を指定することもできますが、省略すれば property 名と同じ名前のライブラリを探します。

次の「boot (module loader) for me」という文が実行された段階で、ライブラリの読み込みが行われます。このコマンドを上の例のように property の値として設定すると、property の値はコンパイル時に評価されますので、ライブラリの読み込みはコンパイル時に行われます。

実行の度に、最新のライブラリを読み込ませたい場合は、run ハンドラの中で実行すれば良いです。

ライブラリを読み込む仕組み

boot (module loader) for me」の動作を詳細に解説します。

  1. まず、module loader コマンドはスクリプティング機能追加 ModuleLoader.osax に定義されているコマンドです。
    • 実行すると、loder スクリプトと呼ぶスクリプトオブジェクトを返します。loader スクリプトと ModuleLoader.osax が協調して動作することが ModuleLoader の肝です。
  2. boot コマンドは、loader スクリプトで定義されているハンドラで、for ラベルに渡されたスクリプトオブジェクトにライブラリをセットアップします。
  3. for ハンドラに渡されたスクリプトオブジェクトから、ModuleLoader.osax が提供している extract dependencies from コマンド を使って、module specifier が設定された property 名 をリストアップします。
    • スクリプトオブジェクトの property のリストを得ることは AppleScript だけでは行えず、C 言語レベルの API を使う必要があるので、スクリプティング機能追加が必要になります。
  4. extract dependencies from で確認されたライブラリをロードします。ロードしたライブラリは、loader スクリプトにキャッシュします。
    • ライブラリの検索は、スクリプティング機能追加で定義されたコマンドで高速に行われます。
  5. ロードしたライブラリを property に設定する。
    • この際に、ライブラリを設定する property を文字列で指定する必要がありますが、XAccessor と同じテクニックを用いています。
    • XAccessor を使えば、スクリプトオブジェクトの property 名やレコードのラベルを文字列で、すなわち変数として扱うことができます。
  6. ロードしたライブラリに対しても、上記の方法を再帰的に適用して、ライブラリのライブラリのロード及び property の設定を行います。

なぜ loader スクリプトが必要なのか

上記のようなライブラリをロードする仕組みは、もしかしたら冗長に感じるかもしれません。例えば、次のように property の宣言でダイレクトにライブラリを読み込んでしまえ、と思うかもしれません。

このような使い方も ModuleLoader.osax はできます。でも、この使い方はお勧めできません。なぜなら、Lib11 と Lib12 には Lib1.scpt が重複してロードされてしまうからです。同じ Lib1.scpt からロードされているにもかかわらず、Lib11 と Lib12 には独立したスクリプトオブジェクトが設定されます。

上の例のように、一つのスクリプトの中一つのライブラリをわざわざ別の property に読みことはないと思われます。でも、複数のライブラリを読み込んだ時、それぞれが共通のサブライブラリを要求したとします。読み込んだライブラリをキャッシュして一元管理する場所がないと、サブライブラリが重複して読み込まれてしまいます。

loader スクリプトを経由させることで、トップレベルのスクリプト及び読み込んだライブラリのあらゆる場所で、ライブラリの同一性が確保することができます。

ライブラリをリロードする仕組み

先に解説したライブラリをロードする仕組みだけだと、いろいろ問題が発生します。まず、どんな問題が発生するのか解説したのち、それをどのように解決しているのか解説します。

まず一つ目の問題は、上の仕組みだけだとライブラリを一度しか読み込むことができなくなります。例えば、「boot (module loader) for me」 文を run ハンドラの中で実行するようにして、スクリプトが起動されるたびにライブラリの読み込みが行われて欲しいと考えます。しかし、property にライブラリが設定されると、その property はスクリプトの終了ともに保存されます。二回目の実行時に boot (module loader) for me 文 が実行された時、 property には module specifier ではなく前回の実行時にライブラリとして読み込んだスクリプトオブジェクトが居座っていますから、ライブラリの読み出しが行われないことになります。

ライブラリのライブラリのロードにも問題が発生します。テストコードを含んだライブラリを開発したとします。テストコードの実行(ライブラリを単体のスクリプトとして実行する)を行うと、ライブラリが依存しているサブライブラリの読み込みが行われて、property に設定しされた module specifer がスクリプトオブジェクトにすり替えられます。そして、そのまま保存してしまったとします。この場合、サブライブラリを読み込んでしまったライブラリをロードするとサブライブラリの読み込みは行われないことになります。

でも、安心してください。ModuleLoader には上記の問題を解決する仕組みがあります。もちろん、ユーザーは何も意識する必要はありません。

loader スクリプトは property へライブラリの設定を行うとともに、__module_dependencies__ というglobal 変数の中に、読み込んだライブラリの情報及び設定した property 名を記録します。global 変数はトップレベルのスクリプトの property と同じ意味です。普通、__module_dependencies__ という property は定義さてれていないでしょうから、新しい property が追加されることになります。

一回目のライブラリの読み込みと同時に、propery に設定されていた module specifier の情報は、__module_dependencies__ に移されます。二回目のライブラリのリロード時は property ではなく__module_dependencies__ からライブラリに関する情報を取得するように動作します。

このように、ModuleLoader はユーザーが意図したようにライブラリのロードが行えます。

ModuleLoader と AppleScript Libraries の類似性

OS X 10.9 から、標準搭載の AppleScript のライブラリシステム、通称 AppleScript Libraries は言語を拡張していますから、スクリプティング機能追加では真似できないところがあります。しかし、本質的なところは非常によく似ていると感じます。特に、use 構文を使った場合は瓜二つです。use 構文でライブラリを指定すると、property の定義と同値になります。

また、use 構文を使うと、required import items という隠しプロパティが生成され、use 構文で指定した項目が格納されています。ModuleLoader がプロパティ __module_dependencies__ を生成することと、とてもよく似ています。

AppleScript Libraries ModuleLoader
基本構文
use Lib1 : script "Lib1"
property Lib1 : module
boot (module loader) for me
隠しプロパティ
required import items
-- {{item:script "Lib1" of «script»}}
__module_dependencies__
-- {{class:dependency info, name:"Lib1", module specifier:{class:module specifier, name:"Lib1"}}}

AppleScript Libraries では、ModuleLoader の「boot (module loader) for me」に相当する部分は、当たり前かもしれませんがないですね。ModuleLoader は言語の拡張ではないので、モジュールの読み込みを明示的に実行しなければなりません。これを煩わしいという見方もできますが、ユーザー側でライブラリの読み込みタイミングを指定できる、もしくは明示的にリロードできるなど、自由度があるという捉え方だってできます。というか、そう捉えてください。

ModuleLoader で AppleScript Library をロードする

ほとんどの用途では、AppleScript Libraries を素直に使えば足りるのだろうと思います。しかし、ModuleLoader を使えば、コンパイル時にすべてのライブラリのロードを済ましてしまうということができます。そうすれば、ライブラリが用意されていない他の Mac に持って行っても動作させることができます。

そこで、ModuleLoader に AppleScript Libraries の構文を解釈できるように拡張してみました。つまり、従来の ModuleLoader のライブラリ指定方法である「property Lib1 : module」の代わりに、 「use Lib1 : script "Lib1"」 を使えるようにしてみました。

use scripting additions
use Lib1 : script "Lib1"
property _ : boot (module loader) for me

パッケージの中の Script Librareis フォルダにライブラリを配置するという方法もありますが、 ModuleLoader で読み込ませてしまう方が簡単です。だいたい、パッケージ内にもれなくライブラリをコピーできたか、確認するのは難しそうです。

まだ、ドキュメントが整備されていませんが、お試していただけると嬉しいです。

ModuleLoader 2.3.4 からの変更点は次の二つです。

  • ~/Library/Script Libries, /Library/Script Libries からもライブラリを探します。
  • AppleScript Libraries の use 構文によって作られた property を解釈して、ライブラリをロードします。

機能的には、二つだけですが内部の実装は非推奨は古い API を置き換えて徹底的に近代化しています。長くメンテナンスできるコードベースになっていると思います。

すでに十分に安定して使えます。Pelease check it out !

自分の公開しているソフトは皆そうですが、ModuleLoader のソースコードは公開しています。気に入らないことがあったら、自分で修正してみてください。ソースコードでわからないことがあったら質問していただいても構いません。できる限りお付き合いします。

2016.06.24

13:56:18Permanent Link

AppleScript ことはじめ」を書き直しました。

AppleScript で開発というより使い始めるための入門文書「AppleScript ことはじめ」をアップデートしました。

内容が、Mac OS X 10.5 Leopard 頃の古い内容だったので、最新の状況に合わせて全面アップデートしました。

画像も Retina ディスプレイを意識して、2倍の解像度で用意しています。

AppleScript の文法をいきなり覚えるのではなく、web で拾ってきたサンプルコードの活用ができるようになってくれれば良いな、と思っています。

2016.06.09

21:48:48Permanent Link

メール.app で選択されているで選択されているメッセージの添付ファイルを IMAP サーバーからダウンロードする(添付ファイルの文字化け回避)

OS X 付属のメール.app には昔から添付ファイルが文字化けするという持病がある。OS X 10.8 までは、MIMEfix という文字化けを修正してくれる秀逸なプラグインがあり、重宝していた。大変残念なことに開発が停止してしまい、OS X 10.9 以降に対応した MIMEfix プラグインが存在しない。

仕様がないから、iCloud の Web メールから添付ファイルをダウンロードしていた。大変めんどくさい。Web メールだと文字化けしないのだから、技術的に難しいことがあるわけでもない。単純に Apple の怠慢でバグが放置されていると思われる(しかし、そうとも言い切れない事情は後ほど)。

添付ファイルのファイル名は、MIME の Content-Disposition: フィールドの filename パラメータに RFC2231 もしくは MIME B でエンコーディングされて記述される。添付ファイルのファイル名が長いと、filename パラメータは複数行に分割されることがある。自分の調べた限りでは、filename フィールドが MIME B でエンコーディングされて、なおかつ複数行に分割されている場合は、ほとんど文字化けが起きる。しかし、そうでない場合もわずかにあるので悩ましい。自分が受け取る添付ファイルのファイル名は、ほとんどは MIME B でエンコードされているので文字化けしまくりである。

ちなみに、メール.app は添付ファイルのファイル名を RFC2331 でエンコードしている。そして、どんなに長いファイル名でも複数行に分割したりしない。Thunderbird もメール.appと同じように、RFC2331 でエンコードするとのこと。そして、メール.app と違ってファイル名が長いと複数行に分割するとのこと。その昔は、Thunderbird から複数行に分割されてエンコードされた添付ファイル名も文字化けしていたが、自分の試した限りでは OS X 10.11 のメール.app ではバグフィックスされている模様。

先ほど、文字化けするのは Apple の怠慢などと偉そうなことを言ったが、そもそも添付ファイル名は MEME B ではなく、RFC2331でエンコードしなければならない決まりになっている。MIME B でファイル名をエンコードして送りつけてくる 行儀の悪いWindows 系のメーラーが諸悪の根源である、というのが本当のところで、単純に Apple をせめてはかわいそうとも思う。

そうも言っていられないので、手軽に文字化けしていない添付ファイルを入手すべく、メール.app で選択されいるメッセージの添付ファイルを IMAP サーバーからダウンロードするスクリプトを書いた。もっぱら、ruby で書き始めたが、半分以上がヒアドキュメントで埋め込まれた AppleScript という不恰好なものになってしまった。

なお、実行には、gem で Mail ライブラリをインストールする必要があります。

#!/usr/bin/env ruby
# coding: utf-8

# Download attachments of selected message in Apple Mail.app
# form IMAP server.

require 'net/imap'
require 'pp'
require 'mail'
require 'yaml'
require 'pathname'
require 'optparse'

def main
  opts = ARGV.getopts('', 'raw_attachments', 'source')
  
  minfo = mail_info
  if minfo.nil? then
    exit 0
  end
  imap = Net::IMAP.new(minfo['server'], minfo['port'], minfo['use_ssl'])
  begin
    imap.login(minfo['user'], minfo['password'])
  rescue => e
    display_alert("Failed to login with error: #{e.message}")
    exit 0
  end
  imap.select(minfo['mailbox'])
  msg_ids = imap.search(["HEADER", "Message-Id", minfo['message_id']])
  a_msg = imap.fetch(msg_ids[0], "RFC822")
  if (opts['source']) then
    print a_msg[0].attr['RFC822']
  end
  m = Mail.new(a_msg[0].attr["RFC822"])
  if m.multipart? then
    saved_files = []
    m.attachments.each do |attachment|
      # 添付ファイルの種類とファイル名
      if opts['raw_attachments'] then
        print attachment
        next
      end
      
      # 添付ファイルの保存処理
      Dir.chdir(minfo['location'])
      filename = attachment.filename
      begin
        File.open(filename, "w+b") {|f|
          f.write attachment.body.decoded
          saved_files.push(Pathname.new(minfo['location'])+filename)
        }
      rescue => e
        #puts "添付ファイルの保存に失敗 #{e.message}"
        display_alert("Failed to save attachments with error: #{e.message}")
      end
    end
    if saved_files.length > 0 then
      final_message(saved_files)
    end
  end

  imap.disconnect
end

def mail_info
  mail_result = `osascript << EOS
tell application id "com.apple.Mail"
  set msgs to selection
  if (count msgs) < 1 then
    display alert "No selected messages."
    return ""
  end if
  
  set msgs_with_attachments to {}
  repeat with a_msg in msgs
    try
      set has_attachments to exists mail attachments of a_msg
    on error
      set has_attachments to true
    end try
    if has_attachments then
      set end of msgs_with_attachments to a_msg
    end if
  end repeat
end tell

set nmsg to count msgs_with_attachments
if nmsg < 1 then
   display alert "No attachements in the selected message."
   return ""
end if

if nmsg > 1 then
  set subject_list to {}
  tell application id "com.apple.Mail"
    set msg_idx to 1
    repeat with a_msg in msgs_with_attachments
      set dt to date received of a_msg
      set end of subject_list to (msg_idx as text) & space ¬
                           & (subject of a_msg) & tab ¬
                           & (short date string of dt) & space ¬
                           & (time string of dt)
      set msg_idx to msg_idx + 1
    end repeat
  end tell
  set a_result to choose from list subject_list with prompt "Choose a message to save attachments" without multiple selections allowed
  if class of a_result is not list then
    return ""
  end if
  set msg_idx to (word 1 of item 1 of a_result) as number
  set target_msg to item msg_idx of msgs_with_attachments
else
  set target_msg to first item of msgs_with_attachments
end if

tell application id "com.apple.Mail"
  tell target_msg
    try
      set has_attachments to exists mail attachments
    on error
      set has_attachments to true
    end try
    if not has_attachments then
      tell current application
        display alert "No attachements in the selected message."
      end tell
      return ""
    end if

    set msgid to message id
    set mbox to its mailbox
    set mbox_name to name of (its mailbox)
    set server_name to server name of account of (its mailbox)
    set use_ssl to uses ssl of account of (its mailbox)
    tell account of (its mailbox)
      set account_name to name
      set user_name to user name
      set port_number to port
    end tell
  end tell
  repeat
    set a_container to (get container of mbox)
    if (class of a_container) is not container then
      exit repeat
    end if
    set mbox to a_container
    set mbox_name to (name of mbox) & "/" & mbox_name
  end repeat
end tell

try
  set a_location to choose folder with prompt "Choose a location to save attachments"
on error
  return ""
end try
try
  set a_result to display dialog "Enter password for " & account_name default answer "" with hidden answer
on error
  return ""
end try
set a_password to text returned of a_result

set lf to ascii character 10
return "---" & lf & ¬
"server: " & server_name & lf & ¬
"port: " & port_number & lf & ¬
"use_ssl: " & (use_ssl as text) & lf & ¬
"user: " & user_name & lf & ¬
"password: " & a_password & lf & ¬
"mailbox: " & mbox_name & lf & ¬
"message_id: " & msgid & lf & ¬
"location: " & (POSIX path of a_location) & lf & ¬
"---"
EOS`
  if mail_result.length == 1 then
    return nil
  end
  #pp mail_result
  
  return YAML.load(mail_result) 
end

def display_alert(msg)
  `osascript << EOS
display alert "#{msg}"
EOS`
end

def final_message(saved_files)
  saved_files_joined = saved_files.join("\n")
  `osascript << EOS
set file_paths to "#{saved_files_joined}"
set a_result to display alert "Attachments are saved." message file_paths ¬
                   buttons {"Cancel", "Reveal", "OK"}
if button returned of a_result is "Reveal" then
  tell application id "com.apple.finder"
    reveal (paragraph 1 of file_paths as POSIX file)
    activate
  end tell
end if
EOS`
end

main

もう、Thunderbird に乗り換えようかな?

2016.05.29

23:48:04Permanent Link

ヨドバシ.com でノート PC の液晶保護フィルムを購入したら、梱包が衝撃的だった

ヨドバシ.com から大きなダンボール箱が届いた。

注文したのは液晶保護フィルムのはず。何か間違ったものが届いたのか、最近の液晶保護フィルムは激厚なのか。

中を開けてみると、ほとんど梱包材だった。なんという無駄。こんなものを注文してしまって、罪悪感を感じる

< Previous Topics