version 2.0 パッケージとして tag 化.
@@ -0,0 +1,96 @@ | ||
1 | +これは SIMple Reminder Mail のヘルプメールです。 | |
2 | +機能仕様、制限について述べていますが、バグや未実装の可能性があります。 | |
3 | +また、SimRM plugin for PukiWiki を使用している場合は、 | |
4 | +SimRM のメールアドレスにメールを送信しても何も起こりません。 | |
5 | +注意してください。 | |
6 | + | |
7 | +機能: | |
8 | +・送信して欲しい時刻やヘルプなどは件名で指定し、 | |
9 | + そのフォーマットは次の通りとなります。 | |
10 | + | |
11 | + YYYYMMDDHHmm # e.g. 200612010800 | |
12 | + YYMMDDHHmm | |
13 | + MMDDHHmm | |
14 | + DDHHmm | |
15 | + HHmm | |
16 | + HH # HH 時 00 分にメール送信 | |
17 | + everyhour:mm | |
18 | + everyday:HHmm | |
19 | + everyweek:wHHmm # w には 0〜6 の値が入り、0 が日曜日 | |
20 | + everymonth:DDHHmm | |
21 | + everyyear:MMDDHHmm | |
22 | + addme:PASSWD # PASSWD は管理者から教えてもらってください | |
23 | + list | |
24 | + stop:XXXXXX # del:, delete: も可 | |
25 | + | |
26 | + plugin:XXX # plg:XXX も可 | |
27 | + YYYYMMDDHHmm:XXX # XXX はプラグイン名 | |
28 | + YYMMDDHHmm:XXX # XXX:options と続けることが可能 | |
29 | + MMDDHHmm:XXX | |
30 | + DDHHmm:XXX | |
31 | + HHmm:XXX | |
32 | + HH:XXX | |
33 | + everyhour:mm:XXX | |
34 | + everyday:HHmm:XXX | |
35 | + everyweek:wHHmm:XXX | |
36 | + everymonth:DDHHmm:XXX | |
37 | + everyyear:MMDDHHmm:XXX | |
38 | + | |
39 | + memoget:XXX # mg:XXX も可 | |
40 | + memolist # ml も可 | |
41 | + memoput:XXX # mp:XXX も可 | |
42 | + | |
43 | + help | |
44 | + version # vr も可 | |
45 | + | |
46 | + | |
47 | +・件名先頭に addme:PASSWD があると | |
48 | + システムを利用可能なユーザとして登録されます。 | |
49 | + PASSWD はシステムの管理者から教えてもらってください。 | |
50 | + | |
51 | +・件名先頭に help があるとヘルプメールが送信されます。 | |
52 | + | |
53 | +・件名先頭に list があると送信予定のメール一覧が送信されます。 | |
54 | + | |
55 | +・memoput:XXX は XXX というタイトルのメモをシステム上に保存し、 | |
56 | + memoget:XXX で保存したメモを引き出すことが出来ます。 | |
57 | + メモを削除するには、本文を空の内容にした | |
58 | + 件名が memoput:XXX (XXX はメモのタイトル) というメールを送信してください。 | |
59 | + | |
60 | +・件名先頭に memolist があると存在するメモの一覧が送信されます。 | |
61 | + (編集、参照できない他人のメモも一覧に含まれます) | |
62 | + | |
63 | +・件名先頭に stop:XXXXXX があると | |
64 | + 受け付けたメールの配信を停止させることができます。 | |
65 | + stop:XXXXXX の XXXXXX には、 | |
66 | + メールの登録時、または指定時刻送信時に届くメール (every のみ) に | |
67 | + 記述された文字列を入れてください。 | |
68 | + | |
69 | +・plugin:XXX は各種拡張機能を利用することが出来ます。 | |
70 | + 件名が plugin:help のメールを送信することでプラグインの使い方を知ることができます。 | |
71 | + また、どんなプラグインがあるかは plugin:list で知ることができます。 | |
72 | + (help, list プラグインともに、管理者が利用不可にしていることもあります) | |
73 | + | |
74 | + | |
75 | +制限: | |
76 | +・text/plain なメールのみ処理されます。 | |
77 | + それ以外はエラーとなります。 | |
78 | + | |
79 | +・添付ファイル付きメールはエラーとなります。 | |
80 | + | |
81 | +・memo{get,put}:XXX の XXX は /^[a-zA-Z0-9]{1,32}$/ を満たす | |
82 | + 必要があります。 | |
83 | + | |
84 | + | |
85 | +詳細: | |
86 | +・SimRM が送るメールの送信先はユーザから届いたメールヘッダを参照し、 | |
87 | + 以下の項目順序で決定されます。(番号の若い項目が優先される) | |
88 | + | |
89 | + 1. Reply-To | |
90 | + 2. Sender | |
91 | + 3. From | |
92 | + | |
93 | + また、複数人のアドレスが記載されていても問題なく送信します。 | |
94 | + | |
95 | + | |
96 | +以上です。 |
@@ -0,0 +1,6 @@ | ||
1 | +-- | |
2 | + | |
3 | +システムがメンテナンス中のため、 | |
4 | +メールの受付を停止しております。 | |
5 | + | |
6 | +以上です。 |
@@ -0,0 +1,8 @@ | ||
1 | +このシステムのヘルプを参照するには、件名に help と記述したメールを、 | |
2 | +システムのメールアドレスに向けて送信してください。 | |
3 | +なお、このメールの返信先 (Reply-To) は管理者のメールアドレスであるため、 | |
4 | +間違った宛先に送らないようにしてください。 | |
5 | + | |
6 | +また、どうしてもエラーが取れない場合は管理者に連絡を取ってみてください。 | |
7 | + | |
8 | +以上です。 |
@@ -0,0 +1,67 @@ | ||
1 | +既存の Subject フォーマットが使えると、楽で良い。 | |
2 | + | |
3 | +以下、想定する Subject フォーマット。 | |
4 | +============================================= | |
5 | +plugin:PLUGIN_NAME[:PLUGIN_OPTIONS] # plugin: は plg: も可。 | |
6 | + | |
7 | +YYYYMMDDHHmm[:PLUGIN_NAME[:PLUGIN_OPTIONS]] | |
8 | +YYMMDDHHmm[:PLUGIN_NAME[:PLUGIN_OPTIONS]] | |
9 | +MMDDHHmm[:PLUGIN_NAME[:PLUGIN_OPTIONS]] | |
10 | +DDHHmm[:PLUGIN_NAME[:PLUGIN_OPTIONS]] | |
11 | +HHmm[:PLUGIN_NAME[:PLUGIN_OPTIONS]] | |
12 | +HH[:PLUGIN_NAME[:PLUGIN_OPTIONS]] | |
13 | +============================================= | |
14 | +e.g. | |
15 | +plugin:cat:foo_file | |
16 | +10:svn | |
17 | +0101:cat:foo_file | |
18 | +============================================= | |
19 | + | |
20 | +plugin ディレクトリ内の .smplg.rb ファイルがプラグインとして認識される。 | |
21 | +また、拡張子を除いたファイル名 (プラグイン名) は [0-9a-z_]+ を満たすこと。 | |
22 | +例えば plugin ディレクトリが次のような構成を持っていたとする。 | |
23 | + | |
24 | +plugin/ | |
25 | + foo.smplg.rb | |
26 | + foo_utils.rb | |
27 | + foo2.rb | |
28 | + foo3.smplg.rb | |
29 | + foo4.txt.smplg.rb | |
30 | + foo5.smplg.rb.sh | |
31 | + foo6-hoo.smplg.rb | |
32 | + 7foo.smplg.rb | |
33 | + | |
34 | +この場合 foo, foo3, 7foo がプラグインとして認識される。 | |
35 | +- foo4.txt は '.' を含んでいるので無視する | |
36 | +- foo6-hoo は '-' を含んでいるので無視する | |
37 | + | |
38 | + | |
39 | + | |
40 | +[プラグインの要件] | |
41 | +・Ruby スクリプトであること | |
42 | + | |
43 | +・プラグインの名前は [0-9a-z_]+ で構成されていること | |
44 | + | |
45 | +・foo.smplg.rb というファイル名ならば Plugin_foo クラスを持ち、 | |
46 | + さらに Plugin_foo クラスは Plugin クラスから派生したクラスであること | |
47 | + | |
48 | + | |
49 | + | |
50 | +[SimRM 側の実装に関する注意] | |
51 | +・thread 処理すること | |
52 | + これはプラグインが短くはない処理時間が必要であった場合への対応となる | |
53 | + | |
54 | +・プラグインのクラスを生成するには eval("Plugin_foo").new("options") とする | |
55 | + | |
56 | +・plugin が送るメールの宛先可否判定には SimRM の userlist を使う | |
57 | + | |
58 | + | |
59 | + | |
60 | +[TODO] | |
61 | +・web.smplg.rb | |
62 | + net/http, net/https, proxy | |
63 | + (取得物は gzip + base64 で返すつもり。 | |
64 | + オプションに decode_src があると | |
65 | + gzip + base64 の文字列を適切に解凍するスクリプトを送付) | |
66 | +・wwwc.smplg.rb | |
67 | + Web ページに更新があったらメールを送る |
@@ -0,0 +1,89 @@ | ||
1 | +このテキストはいくつかのシステムにおいてのインストール方法を | |
2 | +簡単に記述していますが、詳細、最新の情報は以下のサイトで取得 | |
3 | +してください。 | |
4 | + | |
5 | +SimRM wiki | |
6 | +http://simrm.sourceforge.jp/ | |
7 | + | |
8 | +* 今後、この INSTALL ファイルはメンテナンスのコストを考慮し、 | |
9 | + 削除される可能性があります | |
10 | + | |
11 | +概要 ================================================================= | |
12 | + | |
13 | +SimRM のインストール方法は使用するモードによって異なります。 | |
14 | +モードについては usage.txt を参照してください。 | |
15 | + | |
16 | +====================================================================== | |
17 | + | |
18 | +dcs, dcsm, dcsp モードを使用 ========================================= | |
19 | +デーモン化するため、長期利用する場合に有利です。 | |
20 | + | |
21 | +1. reminder というユーザをシステムに追加 (adduser, useradd などで) | |
22 | +2. reminder ユーザの ${HOME}/bin/ あたりに設置 | |
23 | +3. エディタで config.rb を開き、各種情報を設定 | |
24 | +-- D_USER_NAME が 'reminder' になっていることを確認してください | |
25 | +4. 起動コマンドの記述先はシステムによって異なります | |
26 | + | |
27 | + | |
28 | +------------------------ | |
29 | +OpenBSD での登録方法 (dcsm の箇所は適当に変更すること) | |
30 | +1. /etc/rc.local に以下の行を追加 | |
31 | + | |
32 | +# SimRM (Reminder Mail) | |
33 | +if [ -x /home/reminder/bin/simrm.rb ]; then | |
34 | + echo -n ' simrm'; | |
35 | + /usr/local/bin/ruby /home/reminder/bin/simrm.rb dcsm /var/mail/reminder | |
36 | + if [ $? -ne 0 ]; then | |
37 | + echo -n '[fail]' | |
38 | + fi | |
39 | +fi | |
40 | + | |
41 | +2. /etc/rc.shutdown に以下の行を追加 | |
42 | + | |
43 | +# SimRM (Reminder Mail) | |
44 | +if [ -x /home/reminder/bin/simrm.rb -a -r /var/run/simrm.pid ]; then | |
45 | + echo "stop simrm." | |
46 | + /usr/local/bin/ruby /home/reminder/bin/simrm.rb kill | |
47 | +fi | |
48 | + | |
49 | +3. root で crontab -e を実行し、以下の行を追加 | |
50 | + | |
51 | +41 2 * * * /usr/local/bin/ruby /home/reminder/bin/simrm.rb hup | |
52 | + | |
53 | +4. システムを再起動 | |
54 | + | |
55 | + | |
56 | +------------------------ | |
57 | +Debian Linux での登録方法 | |
58 | +1. root で次のコマンドを実行する | |
59 | + | |
60 | +# cp -i /home/reminder/bin/init.d/simrm /etc/init.d | |
61 | +# !$/simrm start | |
62 | +# update-rc.d simrm defaults | |
63 | + | |
64 | + モードを dcsm 以外にする場合は init.d/simrm の OPT_* を変更すること | |
65 | + | |
66 | +2. root で crontab -e を実行し、以下の行を追加 | |
67 | + | |
68 | +41 2 * * * /home/reminder/bin/simrm.rb hup | |
69 | + | |
70 | +====================================================================== | |
71 | + | |
72 | +mbox モードを使用 ==================================================== | |
73 | +1. reminder というユーザをシステムに追加 (adduser, useradd などで) | |
74 | +2. reminder ユーザの ${HOME}/bin/ あたりに設置 | |
75 | +3. エディタで config.rb を開き、各種情報を設定 | |
76 | +4. chmod 750 simrm.rb を実行 | |
77 | +5. reminder ユーザで crontab -e を実行 | |
78 | + | |
79 | + MAILTO=foo | |
80 | + 18 0-23/4 * * * $HOME/bin/simrm.rb clean | |
81 | + 0-59/1 * * * * $HOME/bin/simrm.rb mbox /var/mail/reminder | |
82 | + 0-59/1 * * * * $HOME/bin/simrm.rb send | |
83 | + | |
84 | + MAILTO にはエラーが起こったときのメール先を記述 | |
85 | +====================================================================== | |
86 | + | |
87 | +-- | |
88 | +$Id$ | |
89 | +[EOF] |
@@ -0,0 +1,46 @@ | ||
1 | +# | |
2 | +# エラーメッセージ | |
3 | +# | |
4 | +# $Id$ | |
5 | +# | |
6 | + | |
7 | +$KCODE = 'e' # !重要! ファイルの文字コードを変更する際はここも変更すること | |
8 | + | |
9 | +ERR_ALREADY_ADDED = 'あなたは既にシステムを利用することができるユーザです' | |
10 | +ERR_CONTENT_TYPE = 'Content-Type が text/plain ではありません' | |
11 | +ERR_DEVMISS_MEMO = '管理者に連絡を取ってみてください: エラーコード ERR_DEVMISS_MEMO' | |
12 | +ERR_FROM_MULTI = 'From フィールドに複数のアドレスが記述されています' | |
13 | +ERR_IMPLEMENTATION = '未実装の機能を呼び出しました' | |
14 | +ERR_LIST_USER = 'list が許されるユーザではありません' | |
15 | +ERR_MEMO_DEL = '指定タイトルのメモ削除に失敗しました: 原因不明' | |
16 | +ERR_MEMO_EXIST_DEL = '削除対象のメモが存在しません' | |
17 | +ERR_MEMO_EXIST_GET = '指定タイトルのメモが存在しません' | |
18 | +ERR_MEMO_MEMBER_DEL = '指定タイトルのメモを削除できるメンバーではありません' | |
19 | +ERR_MEMO_MEMBER_GET = '指定タイトルのメモを参照できるメンバーではありません' | |
20 | +ERR_MEMO_MEMBER_PUT = '指定タイトルのメモは既に存在し、編集できないようになっています' | |
21 | +ERR_MEMO_TITLE = 'メモのタイトルは /^[a-zA-Z0-9]{1,32}$/ を満たす必要があります' | |
22 | +ERR_NO_SUBJECT = '件名が記述されていません' | |
23 | +ERR_NOMATCH_PASSWD = 'パスワードが一致しませんでした' | |
24 | +ERR_OLD_TIME = '少なくとも 5 分後より未来である必要があります' | |
25 | +ERR_PARSE_EVERY_TYPE = '件名から every の種類がパースできませんでした' | |
26 | +ERR_PARSE_EVERY_DATE = '件名から every の指定日時がパースできませんでした' | |
27 | +ERR_PARSE_PLUGIN_DIRECTIVE = 'プラグイン関連の件名が正しくパースできませんでした' | |
28 | +ERR_PARSE_PLUGIN_NAME = 'プラグイン関連の件名正しくパースできませんでした' | |
29 | +ERR_PARSE_SUBJECT = '件名が正しくパースできませんでした' | |
30 | +ERR_PLUGIN_ERROR_MAIL = 'プラグインがエラーを返しました: ' | |
31 | +ERR_PLUGIN_SAVE_MAIL = 'プラグインメールの保存チェックがエラーを返しました: ' | |
32 | +ERR_PLUGIN_EXCEPTION = '該当するプラグインが例外を起こしました' | |
33 | +ERR_PLUGIN_NO_EXIST = '該当するプラグインがありませんでした' | |
34 | +ERR_RM_FILE_CHK = 'stop 対象のファイルが見つかりませんでした' | |
35 | +ERR_RM_FILE_EXP = 'stop 対象のファイル削除に失敗しました' | |
36 | +ERR_STOP_FILE = 'stop の対象名が記述されていません' | |
37 | +ERR_STOP_USER = 'stop が許されるユーザではありません' | |
38 | +ERR_TIME_RANGE = '指定した時間が存在しない時間を示しています' | |
39 | +ERR_TOUCH_FILE = "ファイルを touch できませんでした (原因:権限、ファイルシステムなど)\n" + | |
40 | + ' 管理者に連絡を取ってみてください' | |
41 | +ERR_UNIQ_FILE = "指定した時間帯に予約が溜まっており、ユニークな ID を付与できませんでした\n" + | |
42 | + ' 時間帯をずらすなどしてみてください' | |
43 | +ERR_WRONG_ADDRESS = 'From が不正なアドレスです' | |
44 | + | |
45 | + | |
46 | +__END__ |
@@ -0,0 +1,263 @@ | ||
1 | +# | |
2 | +# ユーティリティモジュール | |
3 | +# | |
4 | +# $Id$ | |
5 | +# | |
6 | +$KCODE = 'e' | |
7 | + | |
8 | +require 'date' | |
9 | +require 'etc' | |
10 | +require 'logger' | |
11 | + | |
12 | +# Logger | |
13 | +$utils_log = nil | |
14 | + | |
15 | +module Utils | |
16 | + # 以降の関数は全てモジュール関数として定義 | |
17 | + module_function | |
18 | + | |
19 | + # 文字列中にメールアドレスがあるかどうかチェックします | |
20 | + # TODO: かなり簡易的 | |
21 | + def exist_address?(str) | |
22 | + return false unless str | |
23 | + return str.index('@') != nil | |
24 | + end | |
25 | + | |
26 | + | |
27 | + # メールヘッダのフィールドからメールアドレスを取得し、 | |
28 | + # そのリストを返す | |
29 | + # TODO: かなり簡易的 | |
30 | + def make_address_list_from_field(str) | |
31 | + return [] unless str | |
32 | + return str.scan(/[^<,\s]+@[^>,\s]+/) | |
33 | + end | |
34 | + | |
35 | + | |
36 | + # ファイルのサイズを 0 にする | |
37 | + # OK -> true | |
38 | + # NG -> false | |
39 | + def do_filesize0(fpath) | |
40 | + begin | |
41 | + f = open(fpath, "w") | |
42 | + f.close | |
43 | + return true | |
44 | + rescue | |
45 | + return false | |
46 | + end | |
47 | + end | |
48 | + | |
49 | + | |
50 | + # 日付が正しいかチェック | |
51 | + # OK -> true | |
52 | + # NG -> false | |
53 | + def day_range_ok?(day, month, year) | |
54 | + r30 = [4, 6, 9, 11] | |
55 | + r31 = [1, 3, 5, 7, 8, 10, 12] | |
56 | + | |
57 | + # 2 月だけは特別に判定してやる必要がある | |
58 | + # leap? は閏年判定メソッド | |
59 | + if month == 2 | |
60 | + max = (Date.new(year).leap?) ? 29 : 28 | |
61 | + return (1 <= day and day <= max) | |
62 | + end | |
63 | + | |
64 | + return (1 <= day and day <= 30) if r30.index(month) | |
65 | + return (1 <= day and day <= 31) if r31.index(month) | |
66 | + raise "Control never reach: check_day_range" | |
67 | + end | |
68 | + | |
69 | + | |
70 | + # 時間が正しいかどうかのチェック | |
71 | + # yaer, month, week, day, hour, min はそれぞれで nil でも可 | |
72 | + # OK -> true | |
73 | + # NG -> false | |
74 | + def time_range_ok?(year, month, week, day, hour, min) | |
75 | + # year は 1 未満でなければヨシとする | |
76 | + return false if year and year < 1 | |
77 | + # month は 1 から 12 まで | |
78 | + return false if month and (month < 1 or 12 < month) | |
79 | + # week は 0 から 6 まで | |
80 | + return false if week and (week < 0 or 6 < week) | |
81 | + # day は month, year の影響を受ける | |
82 | + if day and month and year | |
83 | + return false unless day_range_ok?(day, month, year) | |
84 | + else | |
85 | + return false if day and (day < 1 or 31 < day) | |
86 | + end | |
87 | + # hour は 0 から 23 まで | |
88 | + return false if hour and (hour < 0 or 23 < hour) | |
89 | + # min は 0 から 59 まで | |
90 | + return false if min and (min < 0 or 59 < min) | |
91 | + # OK | |
92 | + return true | |
93 | + end | |
94 | + | |
95 | + # UTC とのオフセットを返す | |
96 | + # 例えば日本ならば "+0900" という文字列が返るはず | |
97 | + # NOTE: 秒は切り捨て | |
98 | + def get_utc_offset_str() | |
99 | + num = Time::now.utc_offset | |
100 | + h = num / 3600 | |
101 | + num = num % 3600 | |
102 | + m = num / 60 | |
103 | + | |
104 | + # 文字列化 | |
105 | + s = sprintf("%+03d", h) | |
106 | + s << sprintf("%02d", m) | |
107 | + return s | |
108 | + end | |
109 | + | |
110 | + # grep 機能を提供する | |
111 | + # 戻り値: | |
112 | + # 見つかった行の配列オブジェクトを返す | |
113 | + # (見つからない場合は要素数 0 の配列オブジェクト) | |
114 | + # Usage: | |
115 | + # grep("/path/to/file", /^StringA:/) | |
116 | + # NOTE: 例外は catch せず、そのまま返す | |
117 | + def grep(fpath, re) | |
118 | + a = [] | |
119 | + File.foreach(fpath) {|line| | |
120 | + a << line if line =~ re | |
121 | + } | |
122 | + return a | |
123 | + end | |
124 | + | |
125 | + | |
126 | + # デーモン化 | |
127 | + # NOTE: クラックされたとき痛いが、cd / はしないでおく | |
128 | + def simple_daemon() | |
129 | + cpid = Process.fork { | |
130 | + Process.setsid | |
131 | + File.open("/dev/null"){|f| | |
132 | + STDIN.reopen f | |
133 | + STDOUT.reopen f | |
134 | + STDERR.reopen f | |
135 | + } | |
136 | + yield | |
137 | + } | |
138 | + return cpid | |
139 | + end | |
140 | + | |
141 | + | |
142 | + # 何もしない | |
143 | + def do_nothing | |
144 | + end | |
145 | + | |
146 | + | |
147 | + # スーパーユーザから一般ユーザに一時的に切り替える | |
148 | + # NOTE: force が false ならば、自身が root (id=0) ではないとき及び | |
149 | + # user が不正のときは何もせずに戻る | |
150 | + def switch_euid(user, force = false) | |
151 | + old_uid = nil | |
152 | + return -1 if (!force && Process.uid != 0) | |
153 | + if (user == nil or user == '') | |
154 | + return yield if force | |
155 | + return -2 | |
156 | + end | |
157 | + | |
158 | + old_uid = Process.euid | |
159 | + Process.euid = Etc.getpwnam(user).uid | |
160 | + return yield | |
161 | + ensure | |
162 | + Process.euid = old_uid if old_uid | |
163 | + end | |
164 | + | |
165 | + | |
166 | + # スーパーユーザ権限を完全に捨てる | |
167 | + # NOTE: root (id=0) でなければ何もせずに戻る | |
168 | + def switch_uid(user) | |
169 | + return -1 if (Process.uid != 0) | |
170 | + return -2 if (user == nil or user == '') | |
171 | + | |
172 | + uid = Etc.getpwnam(user).uid | |
173 | + return Process::UID.change_privilege(uid) | |
174 | + end | |
175 | + | |
176 | + | |
177 | + # custom puts | |
178 | + # | |
179 | + # utils_log に Logger オブジェクトがある場合はログ出力 | |
180 | + # そうでなければ STDERR 出力 (また、そのときはログレベルに関係なく出力される) | |
181 | + # | |
182 | + # NOTE: severity が String のときは、 | |
183 | + # 'fatal', 'error', 'warn', 'info', 'debug' (大文字小文字は問わない) | |
184 | + # の文字列に合わせて適切な Integer 値にする | |
185 | + # | |
186 | + # NOTE: head が false のときは STDERR 出力について | |
187 | + # 'ERROR:', 'WARNING:' などの種別情報を付加しない | |
188 | + def cputs(severity, msg, head = true) | |
189 | + m = msg | |
190 | + s = severity | |
191 | + | |
192 | + if severity.instance_of?(String) | |
193 | + case severity.downcase | |
194 | + when 'fatal' then s = Logger::FATAL | |
195 | + when 'error' then s = Logger::ERROR | |
196 | + when 'warn' then s = Logger::WARN | |
197 | + when 'info' then s = Logger::INFO | |
198 | + when 'debug' then s = Logger::DEBUG | |
199 | + else | |
200 | + s = Logger::FATAL | |
201 | + m = "Sevirity '#{severity}' is NOT recognized: #{msg}" | |
202 | + end | |
203 | + end | |
204 | + | |
205 | + if $utils_log == nil | |
206 | + if head | |
207 | + case s | |
208 | + when Logger::FATAL then m = 'FATAL: ' + msg | |
209 | + when Logger::ERROR then m = 'ERROR: ' + msg | |
210 | + when Logger::WARN then m = 'WARNING: ' + msg | |
211 | + when Logger::INFO then m = 'INFO: ' + msg | |
212 | + when Logger::DEBUG then m = 'DEBUG: ' + msg | |
213 | + else m = 'UNKNOWN: ' + msg | |
214 | + end | |
215 | + end | |
216 | + | |
217 | + STDERR.puts m | |
218 | + else | |
219 | + $utils_log.add(s, m) | |
220 | + end | |
221 | + end | |
222 | + | |
223 | + | |
224 | + # メールの件名で重要なものを検知できるようにする | |
225 | + def important_subject?(sub) | |
226 | + return IMPORTANT_SUBJECT.key?(sub) | |
227 | + end | |
228 | + # Subject Data | |
229 | + IMPORTANT_SUBJECT = { | |
230 | + 'Returned mail: see transcript for details' => true | |
231 | + } | |
232 | + | |
233 | + | |
234 | + # ディレクトリ中のファイル数を取得する | |
235 | + # NOTE: 再帰的にはチェックしない | |
236 | + def dir_filenum(dir_path) | |
237 | + cnt = 0 | |
238 | + Dir.foreach(dir_path) {|item| | |
239 | + cnt += 1 if File.file?(dir_path + '/' + item) | |
240 | + } | |
241 | + return cnt | |
242 | + end | |
243 | + | |
244 | + | |
245 | + # ディレクトリ中の合計ファイルサイズを取得する | |
246 | + # 戻り値: [size, file_num] | |
247 | + # | |
248 | + # NOTE: 戻り値の型に注意すること | |
249 | + # NOTE: 再帰的にはチェックしない | |
250 | + def dir_filesize(dir_path) | |
251 | + s = 0 | |
252 | + cnt = 0 | |
253 | + Dir.foreach(dir_path) {|item| | |
254 | + if File.file?(dir_path + '/' + item) | |
255 | + s += File.size(dir_path + '/' + item) | |
256 | + cnt += 1 | |
257 | + end | |
258 | + } | |
259 | + return s, cnt | |
260 | + end | |
261 | +end | |
262 | + | |
263 | +__END__ |
@@ -0,0 +1,66 @@ | ||
1 | +# | |
2 | +# Mail 拡張クラス | |
3 | +# | |
4 | +# $Id$ | |
5 | +# | |
6 | +require 'mailread' | |
7 | +require "#{LIB_DIR}/utils.rb" | |
8 | + | |
9 | +$KCODE = 'e' | |
10 | + | |
11 | +# mailread の Mail クラスを拡張する | |
12 | +class Mail | |
13 | + include Utils | |
14 | + | |
15 | + # 宛先を解決します | |
16 | + # Reply-To, Sender, From の順です | |
17 | + def resolv_to_address | |
18 | + return self['reply-to'] if exist_address?(self['reply-to']) | |
19 | + return self['sender'] if exist_address?(self['sender']) | |
20 | + return self['from'] if exist_address?(self['from']) | |
21 | + return '' | |
22 | + end | |
23 | + | |
24 | + | |
25 | + # resolv_to_address() から Reply-To を外したモノ | |
26 | + def sender_or_from | |
27 | + return self['sender'] if exist_address?(self['sender']) | |
28 | + return self['from'] if exist_address?(self['from']) | |
29 | + return '' | |
30 | + end | |
31 | + | |
32 | + | |
33 | + # Reply-To, Sender, From のアドレスリストを返す | |
34 | + def get_reply_sender_from | |
35 | + as = [] | |
36 | + as << make_address_list_from_field(self['reply-to']) | |
37 | + as << make_address_list_from_field(self['sender']) | |
38 | + as << make_address_list_from_field(self['from']) | |
39 | + return as.flatten | |
40 | + end | |
41 | + | |
42 | + | |
43 | + # From, Sender, Reply-To に該当するアドレスがあるか? | |
44 | + def is_address_written?(address) | |
45 | + as = get_reply_sender_from | |
46 | + return (as.index(address) != nil) | |
47 | + end | |
48 | + | |
49 | + | |
50 | + # Subject をチェックし、プラグインを利用している場合はその名前とオプションを抜き出す。 | |
51 | + # プラグインを使用しなければ nil を返す。 | |
52 | + def get_plugin_and_opts | |
53 | + return nil, nil if self['subject'] | |
54 | + | |
55 | + s = self['subject'].sub(/^\s+/, '').sub(/\s+$/, '') | |
56 | + unless s =~ /^[0-9]/ or s =~ /^every/ or s =~ /^plugin:/ or s =~ /^plg:/ | |
57 | + return nil, nil | |
58 | + end | |
59 | + | |
60 | + di, pname, opts = s.split(':', 3) | |
61 | + return pname, opts | |
62 | + end | |
63 | +end | |
64 | + | |
65 | + | |
66 | +__END__ |
@@ -0,0 +1,617 @@ | ||
1 | +# | |
2 | +# SimRM メール送信クラス | |
3 | +# | |
4 | +# NOTE: | |
5 | +# テスト時にメールを送らず、標準出力にメール内容を出したいときは | |
6 | +# @@NO_SMTP を true にしてください | |
7 | +# | |
8 | +# $Id$ | |
9 | +# | |
10 | + | |
11 | +require 'net/smtp' | |
12 | +require 'nkf' | |
13 | +require "#{LIB_DIR}/mailext.rb" | |
14 | +require "#{LIB_DIR}/userlist.rb" | |
15 | +require "#{LIB_DIR}/utils.rb" | |
16 | + | |
17 | +$KCODE = 'e' # 文字コード対処 | |
18 | + | |
19 | + | |
20 | +# SMTP サーバへの接続に必要な情報構造体 | |
21 | +class SmtpInfo | |
22 | + attr_reader :helo_domain, :host, :port, :user, :passwd, :authtype | |
23 | + def initialize(helo_domain, host, port, user, passwd, authtype) | |
24 | + @helo_domain, @host, @port, @user, @passwd, @authtype = | |
25 | + helo_domain, host, port, user, passwd, authtype | |
26 | + end | |
27 | +end | |
28 | + | |
29 | + | |
30 | +# ヘルプ、エラー、メンテナンスなどのメールを収めた構造体 | |
31 | +class MailFiles | |
32 | + attr_reader :help, :error, :mainte | |
33 | + def initialize(error, help, mainte) | |
34 | + @error, @help, @mainte = error, help, mainte | |
35 | + end | |
36 | +end | |
37 | + | |
38 | + | |
39 | +# ホワイトリストなどのパス情報構造体 | |
40 | +class ListFiles | |
41 | + attr_reader :userfile, :addmefile | |
42 | + def initialize(userfile, addmefile) | |
43 | + @userfile, @addmefile = userfile, addmefile | |
44 | + end | |
45 | +end | |
46 | + | |
47 | + | |
48 | +# インスタンスを生成すると自動的に SMTP セッションが張られるので、 | |
49 | +# もう使わなくなったら sfree() を呼んでください | |
50 | +class SendingMail | |
51 | + APP = 'SimRM' | |
52 | + TAG = "[#{APP}]" | |
53 | + | |
54 | + # NO_SMTP が true ならば、メールを送らず、標準出力に同内容を出力します | |
55 | + attr_reader :from, :reply, :sinfo, :mailfiles, :listfiles | |
56 | + @@NO_SMTP = false | |
57 | + @@org_mail_start = 'Original Mail Info ------' | |
58 | + @@org_mail_end = '-------------------------' | |
59 | + | |
60 | + def initialize(from, reply, smtpinfo, mailfiles, listfiles) | |
61 | + @from, @reply, @sinfo, @mailfiles, @listfiles = | |
62 | + from, reply, smtpinfo, mailfiles, listfiles | |
63 | + @userlist = UserList.new(listfiles.userfile, listfiles.addmefile) | |
64 | + | |
65 | + # 実際にメールを送信するまでセッションは作らないことにしておく | |
66 | + @session = nil | |
67 | + | |
68 | + # 各ファイルの内容は必要となるまで読み込まない | |
69 | + @helpbody = '' | |
70 | + @errbody = '' | |
71 | + @maintebody = '' | |
72 | + end | |
73 | + | |
74 | + | |
75 | + # SMTP のセッション張りが面倒なので関数化 | |
76 | + def make_smtp_session() | |
77 | + raise unless @sinfo | |
78 | + return Net::SMTP.start( | |
79 | + @sinfo.host, @sinfo.port, | |
80 | + @sinfo.helo_domain, @sinfo.user, | |
81 | + @sinfo.passwd, @sinfo.authtype) | |
82 | + end | |
83 | + private :make_smtp_session | |
84 | + | |
85 | + | |
86 | + # session free | |
87 | + def sfree() | |
88 | + @session.finish if @session | |
89 | + @session = nil | |
90 | + end | |
91 | + | |
92 | + | |
93 | + # 定型化する | |
94 | + def make_error_msg(str) | |
95 | + str = "unknown" unless str | |
96 | + return "Error: " + str | |
97 | + end | |
98 | + private :make_error_msg | |
99 | + | |
100 | + | |
101 | + # 'Name <user@hoge.jp>' の user@hoge.jp のみを返す | |
102 | + # TODO: 抜き出し方が簡易的。調べるのも面倒なのでそのうち。 | |
103 | + def get_address_only(str) | |
104 | + ad = str.dup | |
105 | + ad.sub!(/^.*<(.*)>.*$/, '\1') | |
106 | + return ad | |
107 | + end | |
108 | + private :get_address_only | |
109 | + | |
110 | + | |
111 | + # 送信するメールで使われるヘッダー | |
112 | + # NOTE: 最後の行は空白行であることに注意 | |
113 | + def get_common_headers(mail, to, sub) | |
114 | + hs = <<EOS | |
115 | +To: #{to} | |
116 | +From: #{@from} | |
117 | +Subject: #{sub} | |
118 | +Date: #{Time::now.strftime("%a, %d %b %Y %X %z")} | |
119 | +Content-Type: text/plain; charset=ISO-2022-JP | |
120 | +Content-Transfer-Encoding: 7bit | |
121 | +EOS | |
122 | + hs << "X-Simrm-Saved: #{mail['x-simrm-saved']}\n" if mail['x-simrm-saved'] | |
123 | + hs << "X-Simrm-Caller: #{mail['x-simrm-caller']}\n" if mail['x-simrm-caller'] | |
124 | + hs << "\n" | |
125 | + return hs | |
126 | + end | |
127 | + private :get_common_headers | |
128 | + | |
129 | + | |
130 | + # To フィールドから各メールアドレスを取り出す | |
131 | + # NOTE: 'Name <foo@hoge.jp>' の foo@hoge.jp 部のみを取り出すこと | |
132 | + # TODO: 抜き出し方が簡易的。調べるのも面倒なのでそのうち。 | |
133 | + def get_mail_addresses(to) | |
134 | + ads = to.split(", ") | |
135 | + ads.each { |ad| | |
136 | + ad.strip! | |
137 | + # ここで ad = get_address_only(ad) としても反映されないようだ | |
138 | + ad.sub!(/^.*<(.*)>.*$/, '\1') | |
139 | + } | |
140 | + return ads | |
141 | + end | |
142 | + private :get_mail_addresses | |
143 | + | |
144 | + | |
145 | + # メール送信時の補助関数 | |
146 | + def send_mail_sub(contents, to) | |
147 | + raise if to.index("\n") | |
148 | + | |
149 | + # メールアドレスは , で区切られている (?) ので、各アドレスを取得 | |
150 | + # 前後の空白文字は削除しておく | |
151 | + # TODO: (?) の真偽を確かめること (Thunderbird はそうなっていた) | |
152 | + to_members = get_mail_addresses(to) | |
153 | + | |
154 | + # メールを投げるか、標準出力に出す | |
155 | + if @@NO_SMTP | |
156 | + print "\n- mail start -----------------------------\n" | |
157 | + print contents | |
158 | + print "\n- mail end -------------------------------\n" | |
159 | + else | |
160 | + # 宛先が空ならば送らない | |
161 | + return if to_members.empty? | |
162 | + | |
163 | + # sfree() があるので nil を考慮しておく | |
164 | + @session = make_smtp_session() unless @session | |
165 | + @session.send_mail(contents, get_address_only(@from), to_members) | |
166 | + Utils::cputs('info', "sent mail to #{to_members.join(', ')}") | |
167 | + end | |
168 | + end | |
169 | + private :send_mail_sub | |
170 | + | |
171 | + | |
172 | + # filebody が空文字列のときは、 | |
173 | + # file の内容によって上書きされる | |
174 | + # filetype には "ヘルプファイル" や "メンテナンス周知ファイル" などを入れること | |
175 | + def send_foo_file!(mail, subject, file, filebody, filetype) | |
176 | + # ユーザリストに登録されていなければ送信しない | |
177 | + usrfrom = mail['from'] ? NKF.nkf('-mj', mail['from']) : '' | |
178 | + unless @userlist.send_ok?(usrfrom) | |
179 | + Utils::cputs('info', "#{usrfrom} is denied") | |
180 | + return | |
181 | + end | |
182 | + | |
183 | + # ファイルの本文内容を取得 | |
184 | + if filebody == '' | |
185 | + begin | |
186 | + filebody = File.readlines(file).to_s | |
187 | + rescue | |
188 | + filebody = filetype + "の読み込みに失敗しました。\n" + | |
189 | + '管理者に連絡を取ってみてください。' | |
190 | + end | |
191 | + filebody = NKF.nkf('-j', filebody) | |
192 | + end | |
193 | + | |
194 | + # メール内容 | |
195 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
196 | + usrd = mail['date'] ? mail['date'] : '' | |
197 | + usrsub = mail['subject'] ? NKF.nkf('-mj', mail['subject']) : '' | |
198 | + | |
199 | + # 最初に Reply-To の設定をしたのち、全体を代入 | |
200 | + contents = Utils::exist_address?(@reply) ? "Reply-To: #{@reply}\n" : '' | |
201 | + contents += get_common_headers(mail, to, subject) | |
202 | + contents += <<EOS | |
203 | +#{@@org_mail_start} | |
204 | + Date: #{usrd} | |
205 | + From: #{usrfrom} | |
206 | + Subject: #{usrsub} | |
207 | +#{@@org_mail_end} | |
208 | + | |
209 | +#{filebody} | |
210 | +EOS | |
211 | + # 送信 | |
212 | + send_mail_sub(contents, to) | |
213 | + end | |
214 | + private :send_foo_file! | |
215 | + | |
216 | + | |
217 | + # 指定時刻のメールを送信する関数 | |
218 | + # clean は「指定時刻じゃなくて申し訳ない」なフラグ | |
219 | + # plg_body は plugin を使った時の本文 | |
220 | + def send_time_mail(mail, fname, clean, plg_body = nil) | |
221 | + # ユーザリストに登録されていなければ送信しない | |
222 | + usrfrom = mail['from'] ? NKF.nkf('-mj', mail['from']) : '' | |
223 | + unless @userlist.send_ok?(usrfrom) | |
224 | + Utils::cputs('info', "#{usrfrom} is denied") | |
225 | + return | |
226 | + end | |
227 | + | |
228 | + # ファイルの本文内容を取得 | |
229 | + clean_msg = '' | |
230 | + if clean | |
231 | + clean_msg = "このメールは、指定時刻になんからの理由で送信できなかったメールです。\n\n" | |
232 | + clean_msg = NKF.nkf('-j', clean_msg) | |
233 | + end | |
234 | + | |
235 | + stop_msg = '' | |
236 | + base = File.basename(fname) | |
237 | + if base.index(/\d/) > 0 # 一回限りの送信ではない | |
238 | + stop_msg = "この登録されたメール配信を停止するには\n" + | |
239 | + "stop:" + base + " という件名でメールを送ってください\n\n" | |
240 | + stop_msg = NKF.nkf('-j', stop_msg) | |
241 | + end | |
242 | + body = (plg_body != nil) ? plg_body.to_s : mail.body.to_s | |
243 | + body = NKF.nkf('-j', body) unless body.empty? | |
244 | + | |
245 | + # メール内容 | |
246 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
247 | + sub = "#{TAG} Reminder: " + (mail['subject'] ? mail['subject'] : '') | |
248 | + usrd = mail['date'] ? mail['date'] : '' | |
249 | + usrsub = mail['subject'] ? NKF.nkf('-mj', mail['subject']) : '' | |
250 | + | |
251 | + # 最初に Reply-To の設定をしたのち、全体を代入 | |
252 | + contents = Utils::exist_address?(@reply) ? "Reply-To: #{@reply}\n" : '' | |
253 | + contents += get_common_headers(mail, to, sub) | |
254 | + contents += <<EOS | |
255 | +#{clean_msg}#{stop_msg}#{@@org_mail_start} | |
256 | + Date: #{usrd} | |
257 | + From: #{usrfrom} | |
258 | + Subject: #{usrsub} | |
259 | +#{@@org_mail_end} | |
260 | + | |
261 | +#{body} | |
262 | +EOS | |
263 | + # 送信 | |
264 | + send_mail_sub(contents, to) | |
265 | + end | |
266 | + | |
267 | + | |
268 | + # ヘルプメールを送信する | |
269 | + def send_help(mail) | |
270 | + send_foo_file!(mail, "#{TAG} Help", | |
271 | + @mailfiles.help, @helpbody, | |
272 | + 'ヘルプファイル') | |
273 | + end | |
274 | + | |
275 | + | |
276 | + # メンテナンス周知メールを送信する | |
277 | + def send_maintenance(mail) | |
278 | + send_foo_file!(mail, "#{TAG} Maintenance", | |
279 | + @mailfiles.mainte, @maintebody, | |
280 | + 'メンテナンス周知ファイル') | |
281 | + end | |
282 | + | |
283 | + | |
284 | + # エラーメッセージを送付する | |
285 | + # NOTE: sender か from の人に送る。もし空ならば、reply-to もチェック | |
286 | + def send_error_mail(mail, errmsg) | |
287 | + # ユーザリストに登録されていなければ送信しない | |
288 | + usrfrom = mail['from'] ? NKF.nkf('-mj', mail['from']) : '' | |
289 | + unless @userlist.send_ok?(usrfrom) | |
290 | + Utils::cputs('info', "#{usrfrom} is denied") | |
291 | + return | |
292 | + end | |
293 | + | |
294 | + # エラーメールの本文補助内容を取得 | |
295 | + if @errbody == '' | |
296 | + begin | |
297 | + @errbody = File.readlines(@mailfiles.error).to_s | |
298 | + rescue | |
299 | + @errbody = "エラーファイルの読み込みに失敗しました。\n" + | |
300 | + '管理者に連絡を取ってみてください。' | |
301 | + end | |
302 | + @errbody = NKF.nkf('-j', @errbody) | |
303 | + end | |
304 | + | |
305 | + # 本文を作成する | |
306 | + to = mail.sender_or_from.gsub(/\n/, ' ') | |
307 | + to = mail.resolv_to_address.gsub(/\n/, ' ') if to == '' | |
308 | + errmsg = NKF.nkf('-j', make_error_msg(errmsg)) | |
309 | + usrd = mail['date'] ? mail['date'] : '' | |
310 | + usrsub = mail['subject'] ? NKF.nkf('-mj', mail['subject']) : '' | |
311 | + usrtype = mail['content-type'] ? mail['content-type'].gsub(/\n/, "\n ") : '' | |
312 | + | |
313 | + contents = Utils::exist_address?(@reply) ? "Reply-To: #{@reply}\n" : '' | |
314 | + contents += get_common_headers(mail, to, "#{TAG} Error") | |
315 | + contents += <<EOS | |
316 | +#{errmsg} | |
317 | + | |
318 | +#{@@org_mail_start} | |
319 | + Date: #{usrd} | |
320 | + From: #{usrfrom} | |
321 | + Subject: #{usrsub} | |
322 | + Content-Type: #{usrtype} | |
323 | +#{@@org_mail_end} | |
324 | + | |
325 | +#{@errbody} | |
326 | +EOS | |
327 | + # 送信 | |
328 | + send_mail_sub(contents, to) | |
329 | + end | |
330 | + | |
331 | + # OK メッセージなメールを送信 | |
332 | + def send_ok_mail(mail, to, subject, ok_msg, body) | |
333 | + # ユーザリストに登録されていなければ送信しない | |
334 | + usrfrom = mail['from'] ? NKF.nkf('-mj', mail['from']) : '' | |
335 | + unless @userlist.send_ok?(usrfrom) | |
336 | + Utils::cputs('info', "#{usrfrom} is denied") | |
337 | + return | |
338 | + end | |
339 | + | |
340 | + # メール内容 | |
341 | + usrd = mail['date'] ? mail['date'] : '' | |
342 | + usrsub = mail['subject'] ? NKF.nkf('-mj', mail['subject']) : '' | |
343 | + usrrep = mail['reply-to'] ? NKF.nkf('-mj', mail['reply-to']) : '' | |
344 | + | |
345 | + # 最初に Reply-To の設定をしたのち、全体を代入 | |
346 | + contents = Utils::exist_address?(@reply) ? "Reply-To: #{@reply}\n" : '' | |
347 | + contents += get_common_headers(mail, to, subject) | |
348 | + contents += <<EOS | |
349 | +#{ok_msg} | |
350 | + | |
351 | +#{@@org_mail_start} | |
352 | + Date: #{usrd} | |
353 | + From: #{usrfrom} | |
354 | + Subject: #{usrsub} | |
355 | + Reply-To: #{usrrep} | |
356 | +#{@@org_mail_end} | |
357 | + | |
358 | +#{body} | |
359 | +EOS | |
360 | + # 送信 | |
361 | + send_mail_sub(contents, to) | |
362 | + end | |
363 | + private :send_ok_mail | |
364 | + | |
365 | + | |
366 | + # メールを受理したことを通知 | |
367 | + # このときは Sender, From の人だけにメールする | |
368 | + def send_accept(fname) | |
369 | + f = File.open(fname) | |
370 | + mail = Mail.new(f) | |
371 | + | |
372 | + # ファイルの本文内容 | |
373 | + ok_msg = "次の情報を持つメールを登録しました。\n" + | |
374 | + '取り消すには stop:' + File.basename(fname) + | |
375 | + ' という件名でメールを送ってください。' | |
376 | + ok_msg = NKF.nkf('-j', ok_msg) | |
377 | + # プラグインから呼ばれた場合はその旨を記述 | |
378 | + if mail['x-simrm-caller'] | |
379 | + tmp_s = "\n* このメールは '" + mail['x-simrm-caller'] + "' を介して作成されました" | |
380 | + ok_msg << NKF.nkf('-j', tmp_s) | |
381 | + end | |
382 | + | |
383 | + body = mail.body.to_s | |
384 | + body = NKF.nkf('-j', body) unless body.empty? | |
385 | + | |
386 | + to = mail.sender_or_from.gsub(/\n/, ' ') | |
387 | + sub = "#{TAG} Accepted: " + (mail['subject'] ? mail['subject'] : '') | |
388 | + | |
389 | + # 実際の処理 | |
390 | + send_ok_mail(mail, to, sub, ok_msg, body) | |
391 | + | |
392 | + # 後始末 | |
393 | + f.close | |
394 | + end | |
395 | + | |
396 | + | |
397 | + # メモメールの受信を通知 | |
398 | + def send_accept_for_memo(fname) | |
399 | + f = File.open(fname) | |
400 | + mail = Mail.new(f) | |
401 | + | |
402 | + # ファイルの本文内容 | |
403 | + ok_msg = "次の情報を持つメモを登録しました。\n" + | |
404 | + '削除するには同じ件名で本文が空のメールを送ってください。' | |
405 | + ok_msg = NKF.nkf('-j', ok_msg) | |
406 | + # プラグインから呼ばれた場合はその旨を記述 | |
407 | + if mail['x-simrm-caller'] | |
408 | + tmp_s = "\n* このメールは '" + mail['x-simrm-caller'] + "' を介して作成されました" | |
409 | + ok_msg << NKF.nkf('-j', tmp_s) | |
410 | + end | |
411 | + | |
412 | + body = mail.body.to_s | |
413 | + body = NKF.nkf('-j', body) unless body.empty? | |
414 | + | |
415 | + to = mail.sender_or_from.gsub(/\n/, ' ') | |
416 | + sub = "#{TAG} Accepted Memo: " + (mail['subject'] ? mail['subject'] : '') | |
417 | + | |
418 | + # 実際の処理 | |
419 | + send_ok_mail(mail, to, sub, ok_msg, body) | |
420 | + | |
421 | + # 後始末 | |
422 | + f.close | |
423 | + end | |
424 | + | |
425 | + | |
426 | + # 現在のメモを送信 | |
427 | + def send_memo(fname, from_mail) | |
428 | + f = File.open(fname) | |
429 | + memo_mail = Mail.new(f) | |
430 | + | |
431 | + body = memo_mail.body.to_s | |
432 | + body = NKF.nkf('-j', body) unless body.empty? | |
433 | + | |
434 | + d = Time::now.strftime("%a, %d %b %Y %X %z") | |
435 | + to = from_mail.resolv_to_address.gsub(/\n/, ' ') | |
436 | + subject = "#{TAG} Memo: " + | |
437 | + (memo_mail['subject'] ? memo_mail['subject'].sub(/^.*?:/, '') : '') | |
438 | + | |
439 | + # 最初に Reply-To の設定をしたのち、全体を代入 | |
440 | + contents = Utils::exist_address?(@reply) ? "Reply-To: #{@reply}\n" : '' | |
441 | + contents += get_common_headers(memo_mail, to, subject) | |
442 | + contents += body | |
443 | + | |
444 | + # 送信 | |
445 | + send_mail_sub(contents, to) | |
446 | + | |
447 | + # 後始末 | |
448 | + f.close | |
449 | + end | |
450 | + | |
451 | + | |
452 | + # メモリストのメールを送る | |
453 | + # memolist: メモ名のリスト | |
454 | + def send_memolist_mail(mail, memolist) | |
455 | + # ファイルの本文内容 | |
456 | + ok_msg = "登録されているメモの一覧です。" | |
457 | + ok_msg = NKF.nkf('-j', ok_msg) | |
458 | + | |
459 | + if memolist.size == 0 | |
460 | + body = '登録されているメモはありません' | |
461 | + else | |
462 | + body = "" | |
463 | + memolist.each { |m| | |
464 | + body += "- #{m}\n" | |
465 | + } | |
466 | + end | |
467 | + body = NKF.nkf('-j', body) unless body.empty? | |
468 | + | |
469 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
470 | + i = memolist.size | |
471 | + sub = "#{TAG} Memo List: #{i} item" + (i > 1 ? 's' : '') | |
472 | + | |
473 | + send_ok_mail(mail, to, sub, ok_msg, body) | |
474 | + end | |
475 | + | |
476 | + | |
477 | + # メモの削除を通知 | |
478 | + def send_delete_for_memo(mail) | |
479 | + mtitle = (mail['subject'] ? mail['subject'].sub(/^.*?:/, '') : '') | |
480 | + | |
481 | + # ファイルの本文内容 | |
482 | + ok_msg = "メモ " + mtitle + " を削除しました。" | |
483 | + ok_msg = NKF.nkf('-j', ok_msg) | |
484 | + | |
485 | + body = '' | |
486 | + | |
487 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
488 | + sub = "#{TAG} Deleted Memo: " + mtitle | |
489 | + | |
490 | + # 実際の処理 | |
491 | + send_ok_mail(mail, to, sub, ok_msg, body) | |
492 | + end | |
493 | + | |
494 | + | |
495 | + # stop メールを受理したことを通知 | |
496 | + # fname は basename であることに注意 | |
497 | + def send_stop_ok(mail, fname) | |
498 | + # ファイルの本文内容 | |
499 | + ok_msg = "次の情報を持つメールの配信を停止しました。" | |
500 | + ok_msg = NKF.nkf('-j', ok_msg) | |
501 | + | |
502 | + body = mail.body.to_s | |
503 | + body = NKF.nkf('-j', body) unless body.empty? | |
504 | + | |
505 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
506 | + sub = "#{TAG} Stopped: " + fname | |
507 | + | |
508 | + # 実際の処理 | |
509 | + send_ok_mail(mail, to, sub, ok_msg, body) | |
510 | + end | |
511 | + | |
512 | + | |
513 | + # addme メールを受理したことを通知 | |
514 | + # TODO: 本当は Sender にも対応した方がいいかもしれない | |
515 | + def send_added(mail) | |
516 | + # ファイルの本文内容 | |
517 | + ok_msg = "次の From に記載されたアドレスを、システム利用可能なアドレスとして登録しました。" | |
518 | + ok_msg = NKF.nkf('-j', ok_msg) | |
519 | + | |
520 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
521 | + sub = "#{TAG} Added You" | |
522 | + | |
523 | + # 実際の処理 (body は空文字列でいいはず) | |
524 | + send_ok_mail(mail, to, sub, ok_msg, '') | |
525 | + end | |
526 | + | |
527 | + | |
528 | + # list メールを送信する | |
529 | + # sub_body は subject と body のペア配列 | |
530 | + def send_list_mail(mail, sub_body) | |
531 | + # ファイルの本文内容 | |
532 | + ok_msg = "送信予定のメール一覧です。" | |
533 | + ok_msg = NKF.nkf('-j', ok_msg) | |
534 | + | |
535 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
536 | + | |
537 | + if sub_body.empty? | |
538 | + body = '現在、送信予定のメールはありません。' | |
539 | + body = NKF.nkf('-j', body) | |
540 | + else | |
541 | + # メール本文を作成する | |
542 | + body = '' | |
543 | + # まずは件名をリストする | |
544 | + sub_body.each do |pair| | |
545 | + body << '- ' + pair[0] + "\n" | |
546 | + end | |
547 | + body << "\n" | |
548 | + # 送信予定メールの本文をリストしていく | |
549 | + sub_body.each do |pair| | |
550 | + body << "-----------------------------------------------\n" | |
551 | + body << "Subject: #{pair[0]}\n\n" | |
552 | + body << pair[1] | |
553 | + body << "-----------------------------------------------\n\n" | |
554 | + end | |
555 | + end | |
556 | + i = sub_body.size | |
557 | + sub = "#{TAG} List: #{i} item" + (i > 1 ? 's' : '') | |
558 | + | |
559 | + # 実際の処理 (body は空文字列でいいはず) | |
560 | + send_ok_mail(mail, to, sub, ok_msg, body) | |
561 | + end | |
562 | + | |
563 | + | |
564 | + # バージョン情報を送る (verstr は文字コードが変わる) | |
565 | + # version | |
566 | + def send_version!(mail, verstr) | |
567 | + # ファイルの本文内容 | |
568 | + ok_msg = "SimRM に関するバージョン情報です。" | |
569 | + ok_msg = NKF.nkf('-j', ok_msg) | |
570 | + | |
571 | + if verstr.size == 0 | |
572 | + body = 'バージョン情報を取得できませんでした' | |
573 | + else | |
574 | + body = verstr | |
575 | + end | |
576 | + body = NKF.nkf('-j', body) unless body.empty? | |
577 | + | |
578 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
579 | + sub = "#{TAG} Version" | |
580 | + | |
581 | + send_ok_mail(mail, to, sub, ok_msg, body) | |
582 | + end | |
583 | + | |
584 | + | |
585 | + def send_plugin_result(mail, pname, body) | |
586 | + # ファイルの本文内容 | |
587 | + ok_msg = "プラグイン '#{pname}' の実行結果です。" | |
588 | + ok_msg = NKF.nkf('-j', ok_msg) | |
589 | + | |
590 | + to = mail.resolv_to_address.gsub(/\n/, ' ') | |
591 | + sub = "#{TAG} Plugin: #{pname}" | |
592 | + | |
593 | + send_ok_mail(mail, to, sub, ok_msg, body) | |
594 | + end | |
595 | + | |
596 | + | |
597 | + # userlist のラッパー関数群 | |
598 | + def send_ok?(mail) | |
599 | + usrfrom = mail['from'] ? NKF.nkf('-mj', mail['from']) : '' | |
600 | + return @userlist.send_ok?(usrfrom) | |
601 | + end | |
602 | + | |
603 | + # addme メールの From を登録する | |
604 | + # OK -> 0 | |
605 | + # 既に記述済み -> 1 | |
606 | + # 不正なアドレス -> -1 | |
607 | + def add_addme_user(mail) | |
608 | + usrfrom = mail['from'] ? NKF.nkf('-mj', mail['from']) : '' | |
609 | + @userlist.add_addme_user!(usrfrom) | |
610 | + end | |
611 | + | |
612 | + def write_back_addme() | |
613 | + @userlist.write_back_addme() | |
614 | + end | |
615 | +end | |
616 | + | |
617 | +__END__ |
@@ -0,0 +1,150 @@ | ||
1 | +# | |
2 | +# SimRM ユーザリストチェッククラス | |
3 | +# | |
4 | +# NOTE: | |
5 | +# ユーザリストは 1 行に 1 項目ずつ記述し、 | |
6 | +# @ が含まれていることを確認してください。 | |
7 | +# 行先頭に # があるとコメントとして認識されます。 | |
8 | +# | |
9 | +# NOTE: | |
10 | +# ユーザリストはファイルは EUC で保存。 | |
11 | +# 配列時は JIS. | |
12 | +# | |
13 | +# NOTE: | |
14 | +# いくつかのアドレスは強制的に弾くようにしています。 | |
15 | +# 不都合な点があれば、@@BLACKS を編集してください。 | |
16 | +# | |
17 | +# WARNING: | |
18 | +# userlist に有効なアドレスが 1 つもない場合は全てのアドレスを有効と考えます。 | |
19 | +# | |
20 | +# $Id$ | |
21 | +# | |
22 | + | |
23 | +$KCODE = 'e' # 文字コード対処 | |
24 | + | |
25 | +require 'nkf' | |
26 | + | |
27 | +# 対象ファイルは次の通りとする。 | |
28 | +# ・userlist.txt: 管理者が直接編集するホワイトリスト | |
29 | +# ・addme.txt: addme:PASSWD で追加されるホワイトリスト | |
30 | +class UserList | |
31 | + @@BLACKS = [ | |
32 | + 'MAILER-DAEMON@' | |
33 | + ] | |
34 | + | |
35 | + # listname は管理者が直接編集するホワイトリスト | |
36 | + # addmename は addme:PASSWD メールによって追加されるホワイトリスト | |
37 | + attr_reader :listname, :addmename | |
38 | + | |
39 | + def initialize(listname, addmename) | |
40 | + @whites = [] # whites は addme の内容も含むことになるが、 | |
41 | + @addme = [] # addme は write_back_addme() する必要があるので | |
42 | + # このようにしている。 | |
43 | + | |
44 | + @listname = listname | |
45 | + @addmename = addmename | |
46 | + read_list(listname, addmename) | |
47 | + end | |
48 | + | |
49 | + | |
50 | + # addme ファイルに追加したユーザを記述しておく | |
51 | + def write_back_addme() | |
52 | + raise unless @addmename | |
53 | + return if @addme.empty? | |
54 | + | |
55 | + begin | |
56 | + f = File.open(@addmename, "a") | |
57 | + f.print NKF.nkf('-e', @addme.join("\n")), "\n" | |
58 | + f.close | |
59 | + rescue | |
60 | + STDERR.puts "WARNING: Can't process the addme file" | |
61 | + end | |
62 | + end | |
63 | + | |
64 | + | |
65 | + # list 配列に fname の内容を追加していく | |
66 | + # ファイル操作に失敗した場合は false を返す | |
67 | + # NOTE: EUC -> JIS | |
68 | + def add_to_list!(list, fname) | |
69 | + begin | |
70 | + File.foreach(fname) { |line| | |
71 | + line = NKF.nkf('-j', line) | |
72 | + # 空文字列行、コメント行は取り除く | |
73 | + line.strip! | |
74 | + next if line == '' | |
75 | + next if line.index('#') == 0 | |
76 | + | |
77 | + # 要素追加 | |
78 | + list << line if line.index('@') | |
79 | + } | |
80 | + return true | |
81 | + rescue | |
82 | + return false | |
83 | + end | |
84 | + end | |
85 | + private :add_to_list! | |
86 | + | |
87 | + | |
88 | + # リストの中身を確保する | |
89 | + # @マークがない行は無視 | |
90 | + def read_list(listname, addmename) | |
91 | + add_to_list!(@whites, listname) | |
92 | + add_to_list!(@whites, addmename) | |
93 | + end | |
94 | + private :read_list | |
95 | + | |
96 | + | |
97 | + # addr にメールを送っていいかどうかを判定します | |
98 | + # | |
99 | + # TODO: 文字列中に指定文字列があるか確認しているだけなので判定が甘い | |
100 | + # 仕様の検討が必要と思われる | |
101 | + def send_ok?(addr) | |
102 | + # 空文字列は送ってはいけない | |
103 | + return false if addr.empty? | |
104 | + | |
105 | + # ブラックリストに載っているか? | |
106 | + @@BLACKS.each do |b| | |
107 | + return false if addr.index(b) | |
108 | + end | |
109 | + | |
110 | + # ホワイトリストが空ならば、全てのアドレスを有効とする | |
111 | + return true if @whites == [] | |
112 | + | |
113 | + # ホワイトリストに載っているか? | |
114 | + @whites.each do |w| | |
115 | + return true if addr.index(w) | |
116 | + end | |
117 | + | |
118 | + return false | |
119 | + end | |
120 | + | |
121 | + | |
122 | + # ホワイトリストへ追加する | |
123 | + # OK -> 0 | |
124 | + # 既に記述済み -> 1 | |
125 | + # 不正なアドレス -> -1 | |
126 | + # | |
127 | + # NOTE: 引数が変換されるので注意 | |
128 | + # | |
129 | + # NOTE: 先頭が # は駄目 | |
130 | + # @ を含んでいる必要がある | |
131 | + # 空文字列 | |
132 | + def add_addme_user!(addr) | |
133 | + # 改行は変換 | |
134 | + addr.gsub!(/\n/, ' ') | |
135 | + | |
136 | + # 既に記述済み (@whites のみでよい) | |
137 | + return 1 if @whites.index(addr) | |
138 | + | |
139 | + # 不正なアドレス | |
140 | + return -1 if addr.empty? | |
141 | + return -1 unless addr.index('@') | |
142 | + return -1 if addr.index('#') == 0 | |
143 | + | |
144 | + @addme << addr # write_back_addme() 用 | |
145 | + @whites << addr | |
146 | + return 0 | |
147 | + end | |
148 | +end | |
149 | + | |
150 | +__END__ |
@@ -0,0 +1,120 @@ | ||
1 | +# | |
2 | +# SimRM plugin 管理クラス | |
3 | +# | |
4 | +# NOTE: 実装などに疑問を持ったら note/plugin.txt を一読のこと | |
5 | +# | |
6 | +# $Id$ | |
7 | +# | |
8 | + | |
9 | +require "#{LIB_DIR}/utils.rb" | |
10 | +require 'singleton' | |
11 | + | |
12 | +$KCODE = 'e' | |
13 | + | |
14 | +# Pmanager は、高々 1 つだけインスタンスを持つ | |
15 | +class Pmanager | |
16 | + include Singleton | |
17 | + | |
18 | + # 名前違反のパターン | |
19 | + NAME_VIO = /[^0-9a-z_]/ | |
20 | + | |
21 | + def initialize() | |
22 | + @plist = [] # plugin のリスト ("Plugin_" は付いていない) | |
23 | + @folder = "" # plugin ディレクトリ | |
24 | + end | |
25 | + | |
26 | + | |
27 | + # Singleton#instance は引数を取らないため、別途この関数を使って初期化する | |
28 | + # folder には plugin フォルダのパスを渡すこと | |
29 | + # | |
30 | + # NOTE: プラグインを使いたくないときは、引数 use に false を設定すること | |
31 | + def init(folder, use) | |
32 | + if @folder != "" or @plist.size != 0 | |
33 | + Utils::cputs('error', "Pmanager::init() was called again") | |
34 | + return -1 | |
35 | + end | |
36 | + | |
37 | + @folder = folder | |
38 | + return (use ? reload() : 0) | |
39 | + end | |
40 | + | |
41 | + | |
42 | + # 与えられたディレクトリパスをチェックして、 | |
43 | + # プラグインのリストを更新する | |
44 | + # | |
45 | + # 戻り値: 0 -> 正常 | |
46 | + # -1 -> 異常のため、正しく終了していない (ログを参照のこと) | |
47 | + # | |
48 | + # NOTE: init() からも利用される | |
49 | + # TODO: 現状、複数のスレッドから同時に呼ばれることはないのだが、sync させたいところではある | |
50 | + def reload(folder = "") | |
51 | + if (folder == nil or folder == "") && @folder == "" | |
52 | + Utils::cputs('error', "Pmanager::reload() is NOT called properly") | |
53 | + return -1 | |
54 | + end | |
55 | + | |
56 | + if folder == nil or folder == "" | |
57 | + folder = @folder | |
58 | + end | |
59 | + | |
60 | + unless File.directory?(folder) | |
61 | + Utils::cputs('error', "Plugin directory '#{b}' is NOT directory") | |
62 | + return -1 | |
63 | + end | |
64 | + | |
65 | + oldlist = @plist | |
66 | + @plist = [] | |
67 | + # ディレクトリ内の smplg.rb ファイルを検索する | |
68 | + Dir.glob("#{@folder}/*.smplg.rb").each {|f| | |
69 | + b = File.basename(f) | |
70 | + name = b.sub(/\.smplg\.rb$/, '') | |
71 | + if name.index(NAME_VIO) != nil | |
72 | + Utils::cputs('warn', "The name of plugin '#{b}' is not acceptable: this file is ignored") | |
73 | + next | |
74 | + end | |
75 | + if oldlist.include?(name) | |
76 | + Utils::cputs('info', "Plugin '#{b}' will be reloaded, so you will get some unexpected results maybe") | |
77 | + end | |
78 | + | |
79 | + begin | |
80 | + load "#{f}" | |
81 | + rescue LoadError => e | |
82 | + Utils::cputs('error', "Can't load #{f}: #{e.to_s}") | |
83 | + next | |
84 | + end | |
85 | + | |
86 | + begin | |
87 | + unless eval("Plugin_#{name}").use_this_class? | |
88 | + Utils::cputs('info', "Plugin '#{name}' is prohibited") | |
89 | + next | |
90 | + end | |
91 | + rescue => e | |
92 | + Utils::cputs('error', "Can't eval plugin '#{name}': #{e.to_s}") | |
93 | + next | |
94 | + end | |
95 | + | |
96 | + Utils::cputs('debug', "Plugin '#{name}' was loaded") | |
97 | + @plist << name | |
98 | + } | |
99 | + @plist.sort! | |
100 | + return 0 | |
101 | + end | |
102 | + | |
103 | + | |
104 | + # 引数として与えられたプラグイン名が使えるかどうか? | |
105 | + def include?(pname) | |
106 | + return @plist.include?(pname) | |
107 | + end | |
108 | + | |
109 | + | |
110 | + # 現在利用できるプラグインのリスト | |
111 | + # NOTE: 内容変更されても構わないように、文字列を dup している | |
112 | + def get_plugin_names() | |
113 | + ret = [] | |
114 | + @plist.each {|p| ret << p.dup } | |
115 | + return ret | |
116 | + end | |
117 | +end | |
118 | + | |
119 | + | |
120 | +__END__ |
@@ -0,0 +1,74 @@ | ||
1 | +# | |
2 | +# ユーザ設定ファイル | |
3 | +# ・Ruby の文法に従います | |
4 | +# | |
5 | +# $Id$ | |
6 | +# | |
7 | +require 'logger' | |
8 | + | |
9 | +# true を設定すると、現在メンテナンス中である旨を周知するようになります | |
10 | +NOW_MAINTENANCE = false | |
11 | + | |
12 | +# addme コマンドのパスワードです | |
13 | +# 素直に英数字使っておいてください | |
14 | +# 空文字列にするとエラーになります | |
15 | +ADDME_PASSWD = 'Welcome To SimRM' | |
16 | + | |
17 | +# このプログラムを動かす際に使用するデータディレクトリ | |
18 | +DATADIR = '/home/reminder/.simrm' | |
19 | + | |
20 | +# メールを送信するときの From に入ります | |
21 | +FROM_ADDR = 'SimRM <reminder@foo.your.net>' | |
22 | +# 永遠にやりとりが続かないよう、別アドレスを持っておくのがいいだろう | |
23 | +RPLY_ADDR = 'SimRM Admin <reminder_admin@foo.your.net>' | |
24 | + | |
25 | +# メールを送信するときに使う SMTP サーバとポート番号 | |
26 | +SMTP_HOST = '192.168.0.254' | |
27 | +SMTP_PORT = 25 | |
28 | +# SMTP にログインの必要があれば、ユーザ名、パスワードを設定 | |
29 | +# NOTE: 必要が無ければ nil を設定してください | |
30 | +SMTP_USER = nil | |
31 | +SMTP_PASS = nil | |
32 | +# ログイン認証方法を記してください (nil, :login, :plain, :cram_md5) | |
33 | +# NOTE: Yahoo BB は :plain | |
34 | +SMTP_AUTH = nil | |
35 | +# わからなければ特に指定しなくていいと思います | |
36 | +SMTP_HELO = 'localhost.localdomain' | |
37 | + | |
38 | +# pop3 モードを使用する際、メール受信のために接続する POP サーバを設定します | |
39 | +POP3_APOP = false # APOP を使う際は true にしてください | |
40 | +POP3_HOST = '192.168.0.254' | |
41 | +POP3_PORT = 110 | |
42 | +# POP3 のユーザ名、パスワードを設定 | |
43 | +POP3_USER = 'pop3user' | |
44 | +POP3_PASS = 'POP3PASS' | |
45 | +# このシステム宛てだということを認識する To フィールドの文字列 | |
46 | +POP3_TO_ADDR = "SimRM" | |
47 | + | |
48 | +# 返信時の本文最大サイズ | |
49 | +BODY_MAX_SIZE = 1024 * 50 # 50KB | |
50 | + | |
51 | + | |
52 | +# | |
53 | +# NOTE: デーモンの send は毎分 0 秒に行われる | |
54 | +# | |
55 | +# デーモンの clean 間隔 (秒単位) | |
56 | +D_CLEAN_INTERVAL = 60 * 60 * 4 | |
57 | +# デーモンの mbox, pop3 間隔 (秒単位) | |
58 | +D_MBOX_INTERVAL = 30 | |
59 | +D_POP3_INTERVAL = 90 | |
60 | +# プロセスの実行ユーザ名 | |
61 | +# 自身が root の場合、この実行ユーザへ変更します | |
62 | +# 変更しなくてもいいならば '' を設定してください | |
63 | +D_USER_NAME = 'reminder' | |
64 | +# pid を保管するファイル | |
65 | +# (アクセス権がなければ異常終了します) | |
66 | +D_PID_FILE = '/var/run/simrm.pid' | |
67 | +# デーモン時のログレベル | |
68 | +D_LOG_LEVEL = Logger::DEBUG | |
69 | + | |
70 | + | |
71 | +# プラグイン機能を使わないときは false を設定すること | |
72 | +PLUGIN_USE = true | |
73 | + | |
74 | +__END__ |
@@ -0,0 +1,1711 @@ | ||
1 | +#!/usr/bin/env ruby | |
2 | +# | |
3 | +# SIMple Reminder Mail | |
4 | +# | |
5 | +# NOTE: | |
6 | +# 1 行目の実行パスはできればフルパス指定が望ましい。 | |
7 | +# env を使った ruby 起動はシステムの環境変数に左右されるため、 | |
8 | +# 予期せぬ結果となることがある。 | |
9 | +# | |
10 | +# NOTE: | |
11 | +# FileUtils.mv はパーティションが異なる際、 | |
12 | +# copy_entry() を行った後に unlink() をしている。 | |
13 | +# copy_entry() の後に mbox の更新があった場合は | |
14 | +# unlink() が不適切なファイルを削除することになる。 | |
15 | +# mv コマンドも同様の処理をしているのだが、FileUtils.mv にバグがあり、 | |
16 | +# 一部のシステムでは正しく動かないことがあるので、使う際は mv コマンドを使用する。 | |
17 | +# | |
18 | +# See: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/28239 | |
19 | +# | |
20 | +# $Id$ | |
21 | +# | |
22 | + | |
23 | +$KCODE = 'e' # !重要! ファイルの文字コードを変更する際はここも変更すること | |
24 | + | |
25 | +# SimRM が起動した時刻を保存しておく (主にプラグイン用) | |
26 | +START_TIME = Time.now | |
27 | + | |
28 | +# 一部の自作ライブラリは実行ファイルと同じディレクトリか ./lib に置かれる | |
29 | +$: << File.dirname($0) | |
30 | + | |
31 | +# 関係ライブラリの読込み | |
32 | +require 'config.rb' # ユーザ設定 | |
33 | + | |
34 | +require 'fileutils' | |
35 | +require 'logger' | |
36 | +require 'net/pop' | |
37 | +require 'net/smtp' | |
38 | +require 'nkf' | |
39 | +require 'tempfile' | |
40 | +require 'tmpdir.rb' | |
41 | + | |
42 | +# プラグイン、ライブラリの保存ディレクトリを設定し、主要なライブラリを読込む | |
43 | +LIB_DIR = File.dirname($0) + '/lib' | |
44 | +PLUGIN_DIR = File.dirname($0) + '/plugin' | |
45 | +require "#{LIB_DIR}/errmsg.rb" | |
46 | +require "#{LIB_DIR}/mailext.rb" | |
47 | +require "#{LIB_DIR}/pmanager.rb" | |
48 | +require "#{LIB_DIR}/sending.rb" | |
49 | +require "#{LIB_DIR}/utils.rb" | |
50 | +require "#{PLUGIN_DIR}/plugin.rb" | |
51 | + | |
52 | + | |
53 | +# あまり他の人に保存メールを見せたくないため、umask は変更する | |
54 | +UMASK = 027 | |
55 | + | |
56 | +# ヘルプ及びエラーの本文ファイル | |
57 | +HELP_FILE = File.dirname($0) + '/mail/help.txt' | |
58 | +ERR_FILE = File.dirname($0) + '/mail/error.txt' | |
59 | +MAINTE_FILE = File.dirname($0) + '/mail/maintenance.txt' | |
60 | +USAGE_FILE = File.dirname($0) + '/usage.txt' | |
61 | + | |
62 | +# 管理者が直接登録するユーザアドレスファイル (基本的には DATADIR に置きます) | |
63 | +USER_LIST = DATADIR + '/userlist.txt' | |
64 | +# addme:PASSWD によって追加されるアドレスファイル | |
65 | +ADDME_LIST = DATADIR + '/addme.txt' | |
66 | +# USER_LIST のテンプレートファイル | |
67 | +USER_LIST_TEMPLATE = File.dirname($0) + '/template/userlist.txt' | |
68 | +# DATADIR 下のディレクトリ | |
69 | +LOG_DIR = DATADIR + '/log' # ログファイルが置かれるディレクトリ | |
70 | +MEMO_DIR = DATADIR + '/memo' # メモファイルが置かれるディレクトリ | |
71 | +RESERVED_DIR = DATADIR + '/reserve' # 送信予定のメールが収められるディレクトリ | |
72 | +WORKING_DIR = DATADIR + '/working' # 雑多なワーキングディレクトリ | |
73 | + | |
74 | +# 引数 ARGV との関係 | |
75 | +MODE = 0 | |
76 | +MBOX = 1 | |
77 | + | |
78 | +# 第三引数にファイルパスが来るオプション | |
79 | +ARG_THREE_MODES = ['dcsm', 'mbox'] | |
80 | +# デーモンとなるときのモードオプション | |
81 | +DAEMON_MODES = ['dcs', 'dcsm', 'dcsp'] | |
82 | + | |
83 | +# UTC オフセット ("+0900" のような文字列) | |
84 | +UTC_OFFSET_STR = Utils::get_utc_offset_str() | |
85 | + | |
86 | +# ----------------------------------------------------------------------------- | |
87 | + | |
88 | +# plugin の初期化 | |
89 | +# Pmanager は singleton である | |
90 | +def init_plugin() | |
91 | + p = Pmanager.instance | |
92 | + return p.init(PLUGIN_DIR, PLUGIN_USE) | |
93 | +end | |
94 | + | |
95 | + | |
96 | +# force が効いていると強制的に使用方法を表示します | |
97 | +def usage(argc, force = false) | |
98 | + | |
99 | + # 引数の数とモードの対応をわかりやすく | |
100 | + ok = false | |
101 | + ok |= (argc == 1 and ARGV[MODE] == 'clean') | |
102 | + ok |= (argc == 1 and ARGV[MODE] == 'dcs') | |
103 | + ok |= (argc == 2 and ARGV[MODE] == 'dcsm') | |
104 | + ok |= (argc == 1 and ARGV[MODE] == 'dcsp') | |
105 | + ok |= (argc == 1 and ARGV[MODE] == 'hup') | |
106 | + ok |= (argc == 1 and ARGV[MODE] == 'kill') | |
107 | + ok |= (argc == 2 and ARGV[MODE] == 'mbox') | |
108 | + ok |= (argc == 1 and ARGV[MODE] == 'pop3') | |
109 | + ok |= (argc == 1 and ARGV[MODE] == 'send') | |
110 | + ok |= (argc == 1 and ARGV[MODE] == 'stdin') | |
111 | + | |
112 | + if force or !ok | |
113 | + File.readlines(USAGE_FILE, nil).each {|all| | |
114 | + print NKF.nkf('-ed', all) | |
115 | + } | |
116 | + # 多分、準備が出来ていないだろうから、その他のチェックもしておく | |
117 | + return 10 + | |
118 | + check_and_mkdir_wrap() + | |
119 | + check_userlist(USER_LIST, USER_LIST_TEMPLATE) | |
120 | + end | |
121 | + | |
122 | + return 0 | |
123 | +end | |
124 | + | |
125 | + | |
126 | +# from を to に cp し、from をサイズ 0 にする | |
127 | +# to のファイルハンドルを返す | |
128 | +# 読めなければ nil を返す | |
129 | +# NOTE: /var/mail/user は Permission の問題から mv できない | |
130 | +def move_and_open(from, to) | |
131 | + unless FileTest.readable_real?(from) | |
132 | + Utils::cputs('debug', "#{ARGV[MBOX]} is NOT readable.") | |
133 | + return nil | |
134 | + end | |
135 | + # サイズが 0 の場合も nil | |
136 | + return nil unless FileTest.size?(from) | |
137 | + | |
138 | + begin | |
139 | + FileUtils.copy(from, to, {:preserve => true}) | |
140 | + raise unless Utils::do_filesize0(from) | |
141 | + return File.open(to) | |
142 | + rescue | |
143 | + Utils::cputs('error', "Can't handle the files") | |
144 | + exit 20 | |
145 | + end | |
146 | +end | |
147 | + | |
148 | + | |
149 | +# ユーザリストがあるかどうかの確認 | |
150 | +# なければ作成する | |
151 | +def check_userlist(fpath, template) | |
152 | + if FileTest.file?(fpath) and FileTest.readable_real?(fpath) and | |
153 | + FileTest.writable?(fpath) | |
154 | + return 0 | |
155 | + end | |
156 | + | |
157 | + Utils::cputs('warn', "#{fpath} is not found") | |
158 | + # 失敗しても処理を続ける | |
159 | + FileUtils.copy(template, fpath) rescue nil | |
160 | + msg = 'Touch the file ... ' + (FileTest.readable_real?(fpath) ? 'OK.' : 'NG!!') | |
161 | + Utils::cputs('info', msg) | |
162 | + | |
163 | + # 再度確認しておく | |
164 | + if FileTest.file?(fpath) and FileTest.readable_real?(fpath) and | |
165 | + FileTest.writable?(fpath) | |
166 | + return 0 | |
167 | + end | |
168 | + | |
169 | + Utils::cputs('error', "Can't make #{fpath}") | |
170 | + return 25 | |
171 | +end | |
172 | + | |
173 | + | |
174 | +# データディレクトリやユーザリストの確認及び生成 | |
175 | +def check_and_mkdir(dirs) | |
176 | + | |
177 | + # DATADIR の下にもいくつかディレクトリが必要 | |
178 | + dirs.each do |d| | |
179 | + # 既にディレクトリがあるならば良い | |
180 | + next if FileTest.directory?(d) | |
181 | + | |
182 | + if FileTest.exist?(d) | |
183 | + Utils::cputs('error', "#{d} is not directory") | |
184 | + return 30 | |
185 | + end | |
186 | + | |
187 | + # 無いようなので作成する | |
188 | + begin | |
189 | + FileUtils.mkdir(d) | |
190 | + rescue | |
191 | + Utils::cputs('error', "Can't mkdir #{d}") | |
192 | + return 31 | |
193 | + end | |
194 | + end | |
195 | + | |
196 | + # データディレクトリの確認完了 | |
197 | + return 0 | |
198 | +end | |
199 | + | |
200 | + | |
201 | +# check_and_mkdir() をよく利用するディレクトリ用 | |
202 | +def check_and_mkdir_wrap() | |
203 | + return check_and_mkdir([DATADIR, LOG_DIR, MEMO_DIR, RESERVED_DIR, WORKING_DIR]) | |
204 | +end | |
205 | + | |
206 | + | |
207 | +# 他人が覗けないような設定にしておく | |
208 | +# 特にユーザ設定ファイルは重要 | |
209 | +# (svn up すると mode が戻ってしまい、実行に手間が掛かる。 | |
210 | +# よって、ここで変更してしまう) | |
211 | +def check_and_chmod(fpath, mode) | |
212 | + return true if (File::stat(fpath).mode & 0007) == 0 | |
213 | + | |
214 | + # モードが不適切なので変更作業 | |
215 | + Utils::cputs('warn', "Others can manipulate '#{fpath}'") | |
216 | + | |
217 | + ret = true | |
218 | + File.chmod(mode, fpath) rescue ret = false | |
219 | + Utils::cputs((ret ? 'info' : 'warn'), | |
220 | + 'Change the file mode ... ' + (ret ? 'OK.' : 'NG!!')) | |
221 | + return ret | |
222 | +end | |
223 | + | |
224 | + | |
225 | +# 脆弱性に繋がるような設定を判定する | |
226 | +def check_setting() | |
227 | + # umask 設定 | |
228 | + if (UMASK & 0007) != 7 | |
229 | + Utils::cputs('error', "Others can manipulate mail files") | |
230 | + return 40 | |
231 | + end | |
232 | + | |
233 | + # メールの保存ディレクトリチェック | |
234 | + if RESERVED_DIR == File.dirname($0) | |
235 | + Utils::cputs('error', "Subject 'stop:foo' can remove foo in this program directory") | |
236 | + return 41 | |
237 | + end | |
238 | + | |
239 | + # ファイルのモードが適切かどうかチェック | |
240 | + # 不適切ならば変更作業も行う | |
241 | + return 42 unless check_and_chmod($0, 0740) | |
242 | + return 43 unless check_and_chmod(File.dirname($0) + '/config.rb', 0640) | |
243 | + | |
244 | + # パスワード設定 | |
245 | + if ADDME_PASSWD.empty? | |
246 | + Utils::cputs('error', "ADDME_PASSWD is empty") | |
247 | + return 44 | |
248 | + end | |
249 | + | |
250 | + return 0 | |
251 | +end | |
252 | + | |
253 | + | |
254 | +# モードの他に引数チェックを行う | |
255 | +# NOTE: usage() でモードはチェックしていること | |
256 | +def check_args() | |
257 | + # ファイルはフルパスであること | |
258 | + # NOTE: デーモン以外は特に不具合無いはずなので、警告だけにする。 | |
259 | + if ARG_THREE_MODES.index(ARGV[MODE]) | |
260 | + if ARGV[MBOX][0].chr != '/' | |
261 | + Utils::cputs('warn', "mbox path must be full path") | |
262 | + end | |
263 | + end | |
264 | + | |
265 | + return 0 | |
266 | +end | |
267 | + | |
268 | + | |
269 | +# ファイル名が重複しないよう、parse_subject() から呼ばれる | |
270 | +# 適切なファイル名が見つかったならば、即座に touch しておく | |
271 | +# (get_uniq_fname() が何度も同じファイル名を返さないようにするための処置) | |
272 | +# | |
273 | +# 戻り値はフルパスになっているので注意。 | |
274 | +# | |
275 | +# NOTE: | |
276 | +# 送信時はファイルの存在有無だけではなく、 | |
277 | +# サイズが 0 になっていないかどうかのチェックも必要 | |
278 | +def get_uniq_fname(base, datadir) | |
279 | + i = 0 | |
280 | + s = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" | |
281 | + | |
282 | + # ユニークなファイル名が見つかるまでループ | |
283 | + # NOTE: 10000 回実行して、駄目なら諦める | |
284 | + while true | |
285 | + i += 1 | |
286 | + fname = datadir + '/' + base | |
287 | + fname << s[rand(62)] << s[rand(62)] << s[rand(62)] << s[rand(62)] | |
288 | + fname << s[rand(62)] << s[rand(62)] << s[rand(62)] << s[rand(62)] | |
289 | + if FileTest.exist?(fname) | |
290 | + next if i < 10000 | |
291 | + return nil, ERR_UNIQ_FILE | |
292 | + end | |
293 | + | |
294 | + # そのファイル名は無いらしいので、それで touch | |
295 | + # TODO: これで本当に衝突は起こらない? | |
296 | + begin | |
297 | + FileUtils.touch(fname) | |
298 | + break | |
299 | + # touch できなければ実行環境が疑わしいのでエラーにしておく | |
300 | + rescue | |
301 | + return nil, ERR_TOUCH_FILE | |
302 | + end | |
303 | + end | |
304 | + return fname, nil | |
305 | +end | |
306 | + | |
307 | + | |
308 | + | |
309 | +# 時間が正しいか簡易チェック。 | |
310 | +# 想定するファイル名は YYYYmmdd_HHMM_*, | |
311 | +# または HOUR, DAY, WEEK などの every シリーズ | |
312 | +# OK -> true | |
313 | +# NG -> false | |
314 | +def check_time_range(fname) | |
315 | + # HOUR_xx_ が一番短いファイル名と思われる | |
316 | + return false if fname.length < 8 | |
317 | + | |
318 | + year = mon = week = day = hour = min = nil | |
319 | + | |
320 | + # 文字列から時間を取得する | |
321 | + if fname.index(/\d/) == 0 | |
322 | + raise if fname.length != 14 | |
323 | + year, mon, day, hour, min = | |
324 | + fname[0..3].to_i, fname[4..5].to_i, fname[6..7].to_i, | |
325 | + fname[9..10].to_i, fname[11..12].to_i | |
326 | + elsif fname.index("HOUR_") == 0 | |
327 | + raise if fname.length != "HOUR_mm_".length | |
328 | + min = fname[5..6].to_i | |
329 | + elsif fname.index("DAY_") == 0 | |
330 | + raise if fname.length != "DAY_hhmm_".length | |
331 | + hour = fname[4..5].to_i | |
332 | + min = fname[6..7].to_i | |
333 | + elsif fname.index("WEEK_") == 0 | |
334 | + raise if fname.length != "WEEK_w_hhmm_".length | |
335 | + week = fname[5].chr.to_i | |
336 | + hour = fname[7..8].to_i | |
337 | + min = fname[9..10].to_i | |
338 | + elsif fname.index("MONTH_") == 0 | |
339 | + raise if fname.length != "MONTH_dd_hhmm_".length | |
340 | + day = fname[6..7].to_i | |
341 | + hour = fname[9..10].to_i | |
342 | + min = fname[11..12].to_i | |
343 | + elsif fname.index("YEAR_") == 0 | |
344 | + raise if fname.length != "YEAR_MMdd_hhmm_".length | |
345 | + mon = fname[5..6].to_i | |
346 | + day = fname[7..8].to_i | |
347 | + hour = fname[10..11].to_i | |
348 | + min = fname[12..13].to_i | |
349 | + end | |
350 | + | |
351 | + # 時間の範囲が正しいかチェック | |
352 | + return Utils::time_range_ok?(year, mon, week, day, hour, min) | |
353 | +end | |
354 | + | |
355 | + | |
356 | +# 指定時刻よりも古いメールは true を返す | |
357 | +# 想定するファイル名は YYYYmmdd_HHMM_* | |
358 | +def is_old_filename(fname, tm, plus_sec) | |
359 | + limit = Time.at(tm) + plus_sec | |
360 | + ftime = Time.local(fname[0..3], fname[4..5], fname[6..7], | |
361 | + fname[9..10], fname[11..12]) | |
362 | + # limit が大きければ正の値 | |
363 | + return (limit <=> ftime) >= 0 | |
364 | +end | |
365 | + | |
366 | + | |
367 | +# 件名文字列をパースし、指定時刻が記述された分割後ファイル名を返す | |
368 | +# なお、既に同じ指定時刻があってもメール番号を増やし、重複しないようにする | |
369 | +# 指定時刻が 2006-01-08T00:00+09:00 ならば、 | |
370 | +# 20060108_0000_id となります。 | |
371 | +# (本当は W3C-DTF を使い回してファイル名を作りたいけど、 | |
372 | +# ファイルシステムとの親和性がよろしくないので独自にしました) | |
373 | +# | |
374 | +# every 指定は次のファイル名になります。 | |
375 | +# HOUR_15_id # 毎時 15 分に送信 | |
376 | +# DAY_0345_id # 毎日 3 時 45 分に送信 | |
377 | +# WEEK_3_1105_id # 毎週水曜日 3 時 45 分に送信 (0..6; 0 が日曜日) | |
378 | +# MONTH_29_2020_id # 毎月 29 日 20 時 20 分に送信 | |
379 | +# # (その月に存在しない日ならば送信しない) | |
380 | +# YEAR_0302_1420_id # 毎年 3 月 2 日 14 時 20 分 に送信 | |
381 | +# # (その年に存在しない日ならば送信しない) | |
382 | +# | |
383 | +# WARNING: subject の書き換えが起こるので、オリジナルデータは渡さないこと | |
384 | +# | |
385 | +def parse_subject!(subject, datadir, tm) | |
386 | + # 空白等を無くすことで少しだけ補正しておく | |
387 | + # また、文字列中の空白はプラグイン引数の可能性があるため、残しておく | |
388 | + subject.sub!(/^\s+/, '') | |
389 | + subject.sub!(/\s+$/, '') | |
390 | + subject.gsub!(/\n/, '') | |
391 | + | |
392 | + # ':.*' はプラグイン対応 | |
393 | + [ | |
394 | + [/^(\d{8})(\d{4})$/, '\1_\2_'], | |
395 | + [/^(\d{8})(\d{4}):.*$/, '\1_\2_'], | |
396 | + [/^(\d{6})(\d{4})$/, '\1_\2_'], | |
397 | + [/^(\d{6})(\d{4}):.*$/, '\1_\2_'], | |
398 | + [/^(\d{4})(\d{4})$/, '\1_\2_'], | |
399 | + [/^(\d{4})(\d{4}):.*$/, '\1_\2_'], | |
400 | + [/^(\d{2})(\d{4})$/, '\1_\2_'], | |
401 | + [/^(\d{2})(\d{4}):.*$/, '\1_\2_'], | |
402 | + [/^(\d{4})$/, '_\1_'], | |
403 | + [/^(\d{4}):.*$/, '_\1_'], | |
404 | + [/^(\d{2})$/, '_\1'], | |
405 | + [/^(\d{2}):.*$/, '_\1'] | |
406 | + ].each do |a| | |
407 | + # 置換が起こらなければ nil が返るので、それを利用 | |
408 | + if subject.sub!(a[0], a[1]) | |
409 | + case subject.length | |
410 | + when 14 # 指定時刻は完璧なので何もしない | |
411 | + subject | |
412 | + when 12 # 西暦の上 2 桁が足りない | |
413 | + subject = tm.strftime('%Y')[0..1] + subject | |
414 | + when 10 # 西暦が省略されている | |
415 | + subject = tm.strftime('%Y') + subject | |
416 | + when 8 # 西暦、月が省略されている | |
417 | + subject = tm.strftime('%Y%m') + subject | |
418 | + when 6 # 年月日が省略されている | |
419 | + subject = tm.strftime('%Y%m%d') + subject | |
420 | + when 3 # 年月日、0 分が省略されている | |
421 | + subject = tm.strftime('%Y%m%d') + subject + '00_' | |
422 | + else | |
423 | + raise 'program mistake: ' + subject | |
424 | + end | |
425 | + | |
426 | + # 存在しない時間を指しているモノはエラー | |
427 | + return nil, ERR_TIME_RANGE unless check_time_range(subject) | |
428 | + | |
429 | + # 未来の時刻を指していないモノはエラー | |
430 | + return nil, ERR_OLD_TIME if is_old_filename(subject, tm, (5 * 60) - 1) | |
431 | + | |
432 | + # id を取得し、戻る | |
433 | + return get_uniq_fname(subject, datadir) | |
434 | + end | |
435 | + end | |
436 | + | |
437 | + # every シリーズ | |
438 | + [ | |
439 | + [/^everyhour:(\d{2})$/, 'HOUR_\1_'], | |
440 | + [/^everyhour:(\d{2}):.*$/, 'HOUR_\1_'], | |
441 | + [/^everyday:(\d{4})$/, 'DAY_\1_'], | |
442 | + [/^everyday:(\d{4}):.*$/, 'DAY_\1_'], | |
443 | + [/^everyweek:(\d)(\d{4})$/, 'WEEK_\1_\2_'], | |
444 | + [/^everyweek:(\d)(\d{4}):.*$/, 'WEEK_\1_\2_'], | |
445 | + [/^everymonth:(\d{2})(\d{4})$/, 'MONTH_\1_\2_'], | |
446 | + [/^everymonth:(\d{2})(\d{4}):.*$/, 'MONTH_\1_\2_'], | |
447 | + [/^everyyear:(\d{4})(\d{4})$/, 'YEAR_\1_\2_'], | |
448 | + [/^everyyear:(\d{4})(\d{4}):.*$/, 'YEAR_\1_\2_'] | |
449 | + ].each do |a| | |
450 | + if subject.sub!(a[0], a[1]) | |
451 | + # 存在しない時間を指しているモノはエラー | |
452 | + return nil, ERR_TIME_RANGE unless check_time_range(subject) | |
453 | + | |
454 | + # id を取得し、戻る | |
455 | + return get_uniq_fname(subject, datadir) | |
456 | + end | |
457 | + end | |
458 | + | |
459 | + return nil, ERR_PARSE_SUBJECT | |
460 | +end | |
461 | + | |
462 | + | |
463 | +# メールを 1 通ごと保存する関数 | |
464 | +# | |
465 | +# NOTE: | |
466 | +# ・fname はフルパスであること | |
467 | +# ・速攻である必要がないので、copy してからテンポラリファイルを削除すればいいだろう | |
468 | +# ・FileUtils.mv はバグがあるから使えない | |
469 | +# ・File.rename は savedir と workdir が別パーティションの可能性があるので使えない | |
470 | +def save_mail(fname, mail, workdir) | |
471 | + | |
472 | + # まずはメール内容としてヘッダーから作成する | |
473 | + # ・ヘッダーの項目内容またはヘッダー名が 1024 文字以上だったらヘッダーごとカット | |
474 | + # ・・代替物を入れておく | |
475 | + # ・ただし、Sender, From, Reply-To は無条件で残す | |
476 | + # ・ヘッダーは小文字のままで OK | |
477 | + i = 1 | |
478 | + contents = '' | |
479 | + mail.header.each_pair { |k, v| | |
480 | + wk = k.dup | |
481 | + wv = v.gsub(/\n/, "\n ") | |
482 | + if (!['from', 'reply-to', 'sender'].index(wk)) and | |
483 | + (wk.length >= 1024 or wv.length >= 1024) | |
484 | + wk = 'overheadersize' + i.to_s | |
485 | + wv = '' | |
486 | + i += 1 | |
487 | + end | |
488 | + contents << wk << ': ' << wv << "\n" | |
489 | + } | |
490 | + # 自前ヘッダー追加 | |
491 | + contents << "X-Simrm-Saved: " | |
492 | + contents << Time.now.strftime("%a, %d %b %Y %H:%M:%S ") | |
493 | + contents << UTC_OFFSET_STR << "\n" | |
494 | + | |
495 | + # ヘッダー終了 ------------------------ | |
496 | + contents << "\n" | |
497 | + | |
498 | + | |
499 | + # 本文を許容範囲内のサイズに収める | |
500 | + # NOTE: mail.body は少なくとも [] になっているので each は呼べる | |
501 | + size = 0 | |
502 | + mail.body.each do |line| | |
503 | + size += line.length | |
504 | + if size > BODY_MAX_SIZE | |
505 | + contents << "size over\n" | |
506 | + break | |
507 | + end | |
508 | + contents << line | |
509 | + end | |
510 | + | |
511 | + # テンポラリファイルの作成と書き込み | |
512 | + bname = File.basename(fname) | |
513 | + tf = Tempfile.new(bname, workdir) | |
514 | + tf.print contents | |
515 | + tf.close(false) | |
516 | + | |
517 | + FileUtils.copy(tf.path, fname) | |
518 | + Utils::cputs('info', "#{fname} was saved") | |
519 | + | |
520 | + # TempFile は GC によって削除されるのでこのまま戻る | |
521 | +end | |
522 | + | |
523 | + | |
524 | +# 件名文字列をパースし、指定メモ名が書かれた分割後ファイル名を返す | |
525 | +# 戻り値: | |
526 | +# nil, エラーメッセージ -> 失敗 | |
527 | +# fpath, nil -> 書き込み可能 | |
528 | +# | |
529 | +# WARNING: subject の書き換えが起こるので、オリジナルデータは渡さないこと | |
530 | +# | |
531 | +def parse_subject_for_memo!(subject, datadir) | |
532 | + subject.gsub!(/\s/, '') # 空白等を無くすことで少しだけ補正 | |
533 | + | |
534 | + case subject | |
535 | + when /^memoput:/, /^mp:/, /^memoget:/, /^mg:/ | |
536 | + subject.sub!(/^.*?:/, '') | |
537 | + else | |
538 | + return nil, ERR_DEVMISS_MEMO | |
539 | + end | |
540 | + | |
541 | + # メモのタイトルが仕様を満たしているか? | |
542 | + return nil, ERR_MEMO_TITLE unless subject =~ /^[a-zA-Z0-9]{1,32}$/ | |
543 | + | |
544 | + fpath = datadir + '/MEMO_' + subject | |
545 | + return fpath, nil | |
546 | +end | |
547 | + | |
548 | + | |
549 | +# メモファイルのメンバーかどうかをチェックする | |
550 | +def check_memo_member(fpath, mail) | |
551 | + return false unless FileTest.exist?(fpath) | |
552 | + | |
553 | + # 同じファイルが既に存在する場合は Sender, From, Reply-To ヘッダをチェック | |
554 | + f = nil | |
555 | + w_ok = false | |
556 | + begin | |
557 | + f = File.open(fpath) | |
558 | + m = Mail.new(f) | |
559 | + Utils::make_address_list_from_field(mail.sender_or_from).each {|ad| | |
560 | + w_ok = true if m.is_address_written?(ad) | |
561 | + } | |
562 | + rescue | |
563 | + Utils::cputs('warn', "Can't process #{fpath}") | |
564 | + ensure | |
565 | + f.close if f | |
566 | + end | |
567 | + return w_ok | |
568 | +end | |
569 | + | |
570 | + | |
571 | +# メモを削除する関数 | |
572 | +# 戻り値: | |
573 | +# nil -> 削除完了 | |
574 | +# errmsg -> エラーがあった | |
575 | +# | |
576 | +# NOTE: | |
577 | +# ・fpath はフルパスであること | |
578 | +def delete_memo(fpath, mail) | |
579 | + return ERR_MEMO_EXIST_DEL unless FileTest.exist?(fpath) | |
580 | + return ERR_MEMO_MEMBER_DEL unless check_memo_member(fpath, mail) | |
581 | + | |
582 | + begin | |
583 | + File.unlink(fpath) | |
584 | + rescue | |
585 | + Utils::cputs('warn', "Can't unlink #{fpath}") | |
586 | + return ERR_MEMO_DEL | |
587 | + end | |
588 | + | |
589 | + # 正常終了 | |
590 | + return nil | |
591 | +end | |
592 | + | |
593 | + | |
594 | +# メモメールを 1 通ごと保存する関数 | |
595 | +# 戻り値: | |
596 | +# nil -> 保存完了 | |
597 | +# errmsg -> エラーがあった | |
598 | +# | |
599 | +# NOTE: | |
600 | +# ・fpath はフルパスであること | |
601 | +def save_memo(fpath, mail, workdir) | |
602 | + if FileTest.exist?(fpath) | |
603 | + return ERR_MEMO_MEMBER_PUT unless check_memo_member(fpath, mail) | |
604 | + end | |
605 | + | |
606 | + # そのまま使って問題無い | |
607 | + save_mail(fpath, mail, workdir) | |
608 | + return nil | |
609 | +end | |
610 | + | |
611 | + | |
612 | +# 件名が addme のメールを処理する | |
613 | +# OK -> true, nil | |
614 | +# NG -> false, ERRMSG | |
615 | +def process_addme_mail(sm, mail) | |
616 | + raise 'no subject' unless mail['subject'] | |
617 | + | |
618 | + pw = NKF.nkf('-me', mail['subject']) | |
619 | + pw.sub!(/^addme:(.*)$/, '\1') | |
620 | + # エラーメッセージを返すが、実際にエラーメールは飛ばないだろう | |
621 | + return false, ERR_NOMATCH_PASSWD if pw != ADDME_PASSWD | |
622 | + | |
623 | + case sm.add_addme_user(mail) | |
624 | + when 0 | |
625 | + return true, nil | |
626 | + when -1 | |
627 | + return false, ERR_WRONG_ADDRESS | |
628 | + when 1 # 多分、ここには来ないと思うが念のため | |
629 | + return false, ERR_ALREADY_ADDED | |
630 | + else | |
631 | + raise 'Control never reach: process_addme_mail' | |
632 | + end | |
633 | +end | |
634 | + | |
635 | + | |
636 | +# 件名が del, delete, stop のメールを処理する | |
637 | +# 無事に削除できるときは対象者にメールを送る | |
638 | +# OK -> true, nil | |
639 | +# NG -> false, ERRMSG | |
640 | +def process_stop_mail(sm, mail) | |
641 | + # ユーザリストに登録されていなければ送信しない | |
642 | + # (エラーメッセージを返しているが、エラーメールは送信されないだろう) | |
643 | + return false, ERR_STOP_USER unless sm.send_ok?(mail) | |
644 | + | |
645 | + raise 'no subject' unless mail['subject'] | |
646 | + fname = NKF.nkf('-me', mail['subject']) | |
647 | + # わざわざ文字クラスを指定しているのは、 | |
648 | + # マルチバイト対応の正規表現だと \w が全角英数字も引っ掛けてしまうようなので | |
649 | + fname.sub!(/^(del|delete|stop):([0-9a-zA-Z_]*).*$/, '\2') | |
650 | + fname.gsub!(/[+*.\/]/, '') # 悪さしそうな文字はカットしておく | |
651 | + return false, ERR_STOP_FILE if fname.empty? | |
652 | + | |
653 | + # フルパスを作成し、削除 | |
654 | + path = RESERVED_DIR + '/' + fname | |
655 | + | |
656 | + # 対象ファイルの妥当性をチェック | |
657 | + if FileTest.file?(path) and FileTest.owned?(path) and | |
658 | + FileTest.writable?(path) and FileTest.size?(path) | |
659 | + begin | |
660 | + # stop 対象ファイルを開き、削除する旨を送る | |
661 | + f = File.open(path) | |
662 | + stopm = Mail.new(f) | |
663 | + sm.send_stop_ok(stopm, fname) | |
664 | + f.close | |
665 | + | |
666 | + # ここでやっとファイル削除 | |
667 | + File.unlink(path) | |
668 | + return true, nil | |
669 | + rescue | |
670 | + return false, ERR_RM_FILE_EXP | |
671 | + end | |
672 | + end | |
673 | + | |
674 | + # 失敗 | |
675 | + return false, ERR_RM_FILE_CHK | |
676 | +end | |
677 | + | |
678 | + | |
679 | +# メールファイルを開き、それが自分宛であるかどうかをチェックする | |
680 | +# 自分宛のメール: subject, body | |
681 | +# 自分宛ではないメール: nil, nil | |
682 | +# | |
683 | +# NOTE: subject は元メールの件名 | |
684 | +# body はヘッダ内容と本文 (3 行) とする | |
685 | +def parse_for_list(fpath, from) | |
686 | + # 対象ファイルの妥当性をチェック | |
687 | + return nil, nil unless FileTest.file?(fpath) and FileTest.readable?(fpath) | |
688 | + | |
689 | + begin | |
690 | + f = File.open(fpath) | |
691 | + m = Mail.new(f) | |
692 | + return nil, nil if m.header.empty? | |
693 | + | |
694 | + # 自分宛かチェック | |
695 | + ad = m.resolv_to_address.gsub(/\n/, ' ') | |
696 | + unless ad.index(from) | |
697 | + f.close | |
698 | + return nil, nil | |
699 | + end | |
700 | + | |
701 | + # body はとりあえず 3 行分確保 | |
702 | + subject = m['subject'] ? NKF.nkf('-mj', m['subject']) : 'unknown' | |
703 | + body = m.body ? NKF.nkf('-j', m.body[0..2].join) : '' | |
704 | + | |
705 | + f.close | |
706 | + return subject, body | |
707 | + rescue | |
708 | + # 何もしないでおく | |
709 | + end | |
710 | + | |
711 | + return nil, nil | |
712 | +end | |
713 | + | |
714 | + | |
715 | +# 件名が list のメールを処理する | |
716 | +# 送り先が From or Sender に記述されているアドレスと一致するならば、 | |
717 | +# それらをリスト化して送る | |
718 | +# NOTE: 保存されているメール量によるが、処理は結構重そうだ | |
719 | +# OK -> true, nil | |
720 | +# NG -> false, ERRMSG | |
721 | +def process_list_mail(sm, mail) | |
722 | + # ユーザリストに登録されていなければ送信しない | |
723 | + # (エラーメッセージを返しているが、エラーメールは送信されないだろう) | |
724 | + return false, ERR_LIST_USER unless sm.send_ok?(mail) | |
725 | + | |
726 | + # 送信者のアドレスを取得 | |
727 | + from = mail.sender_or_from.gsub(/\n/, ' ') | |
728 | + return false, ERR_WRONG_ADDRESS unless Utils::exist_address?(from) | |
729 | + # From に複数のアドレスがあるならばエラーとする。 | |
730 | + # また、adds は Name <add@res.net> の add@res.net 部分のみを取り出したものである。 | |
731 | + adds = Utils::make_address_list_from_field(from) | |
732 | + return false, ERR_FROM_MULTI unless adds.length == 1 | |
733 | + from = adds[0] | |
734 | + | |
735 | + # ディレクトリのファイル全てを対象とする | |
736 | + # 中身をパースして自分宛ならばその内容を取得 | |
737 | + s_b = [] # -j されていること | |
738 | + Dir.glob(RESERVED_DIR + '/*').each do |fpath| | |
739 | + sub, body = parse_for_list(fpath, from) | |
740 | + if sub and body | |
741 | + s_b << [sub, body] | |
742 | + end | |
743 | + end | |
744 | + | |
745 | + # list メールを送る | |
746 | + sm.send_list_mail(mail, s_b) | |
747 | + return true, nil | |
748 | +end | |
749 | + | |
750 | + | |
751 | +# システムで保持しているメモのリストを返す。 | |
752 | +# 自分に参照、編集権限が無くとも全てのメモのリストを返す。 | |
753 | +def process_memolist_mail(sm, mail) | |
754 | + base = MEMO_DIR + '/MEMO_' | |
755 | + | |
756 | + # ディレクトリのファイル全てを対象とする | |
757 | + # 中身をパースして自分宛ならばその内容を取得 | |
758 | + mlist = [] | |
759 | + Dir.glob(base + '*').each do |fpath| | |
760 | + mlist << fpath.sub(/#{base}/, '') | |
761 | + end | |
762 | + | |
763 | + # memolist メールを送る | |
764 | + sm.send_memolist_mail(mail, mlist) | |
765 | + return true, nil | |
766 | +end | |
767 | + | |
768 | + | |
769 | +# バージョン情報を返す | |
770 | +# TODO: 実行方法によってはスクリプトのフルパスが見える (問題無いか?) | |
771 | +def process_version(sm, mail) | |
772 | + verstr = "Ruby Version: #{RUBY_VERSION}\n" | |
773 | + verstr << "SimRM Scripts Version:\n" | |
774 | + | |
775 | + # 実行ディレクトリと lib ディレクトリ下のスクリプトを走査する | |
776 | + Dir.glob("#{File.dirname($0)}/{.,lib}/*.rb").each {|f| | |
777 | + ret = Utils::grep(f, /\$Id.*\$\s*$/) | |
778 | + if ret.size == 0 | |
779 | + verstr << "#{f}: No Id\n" | |
780 | + else | |
781 | + # #{s} には改行が含まれている | |
782 | + ret.each {|s| verstr << "#{f}: #{s}" } | |
783 | + end | |
784 | + } | |
785 | + | |
786 | + sm.send_version!(mail, verstr) | |
787 | + return true, nil | |
788 | +end | |
789 | + | |
790 | + | |
791 | +# メモを設置 or 編集する | |
792 | +# OK -> true, nil | |
793 | +# NG -> false, ERRMSG | |
794 | +def process_memoput_mail(sm, mail) | |
795 | + # fname はフルパスが入る | |
796 | + fpath, errmsg = parse_subject_for_memo!(mail['subject'].dup, MEMO_DIR) | |
797 | + return false, errmsg if errmsg | |
798 | + | |
799 | + # メモを保存する | |
800 | + # (メールの body が \s のみ, または空文字列で構成されている場合は削除) | |
801 | + if mail.body.to_s.gsub(/\s/, '') == '' | |
802 | + errmsg = delete_memo(fpath, mail) | |
803 | + return false, errmsg if errmsg | |
804 | + | |
805 | + # メモの消去完了を通知する | |
806 | + sm.send_delete_for_memo(mail) | |
807 | + else | |
808 | + errmsg = save_memo(fpath, mail, WORKING_DIR) | |
809 | + return false, errmsg if errmsg | |
810 | + | |
811 | + # メモの保存完了を通知する | |
812 | + sm.send_accept_for_memo(fpath) | |
813 | + end | |
814 | + | |
815 | + return true, nil | |
816 | +end | |
817 | + | |
818 | + | |
819 | +# メモを取得する | |
820 | +# OK -> true, nil | |
821 | +# NG -> false, ERRMSG | |
822 | +def process_memoget_mail(sm, mail) | |
823 | + # fname はフルパスが入る | |
824 | + fpath, errmsg = parse_subject_for_memo!(mail['subject'].dup, MEMO_DIR) | |
825 | + return false, errmsg if errmsg | |
826 | + | |
827 | + # ファイルがあるか? | |
828 | + return false, ERR_MEMO_EXIST_GET unless FileTest.exist?(fpath) | |
829 | + # 参照できるメンバか? | |
830 | + return false, ERR_MEMO_MEMBER_GET unless check_memo_member(fpath, mail) | |
831 | + | |
832 | + # メモを送信 | |
833 | + sm.send_memo(fpath, mail) | |
834 | + return true, nil | |
835 | +end | |
836 | + | |
837 | + | |
838 | +# プラグイン関連の件名文字列をパースし、適切な値に分割する | |
839 | +# | |
840 | +# 戻り値: | |
841 | +# nil, nil, nil, エラーメッセージ -> 失敗 | |
842 | +# directive("plugin", 予約日時など), plugin名、options, nil -> 成功 | |
843 | +# | |
844 | +# NOTE: 戻り値の options は nil となることがある | |
845 | +# | |
846 | +# WARNING: subject の書き換えが起こるので、オリジナルデータは渡さないこと | |
847 | +def parse_subject_for_plugin!(subject) | |
848 | + # 最初と最後にある空白等を無くすことで少しだけ補正 | |
849 | + subject.sub!(/^\s+/, '') | |
850 | + subject.sub!(/\s+$/, '') | |
851 | + subject.gsub!(/\n/, '') | |
852 | + | |
853 | + di = pname = opts = nil | |
854 | + if subject =~ /^every/ | |
855 | + ev, d, pname, opts = subject.split(':', 4) | |
856 | + return nil, nil, nil, ERR_PARSE_EVERY_TYPE if ev == nil | |
857 | + return nil, nil, nil, ERR_PARSE_EVERY_DATE if d == nil | |
858 | + di = "#{ev}:#{d}" | |
859 | + else | |
860 | + # この di には "plugin", "plg" または予約日時が入る | |
861 | + di, pname, opts = subject.split(':', 3) | |
862 | + end | |
863 | + return nil, nil, nil, ERR_PARSE_PLUGIN_DIRECTIVE if di == nil | |
864 | + return nil, nil, nil, ERR_PARSE_PLUGIN_NAME if pname == nil | |
865 | + | |
866 | + return di, pname, opts, nil | |
867 | +end | |
868 | + | |
869 | + | |
870 | +# プラグインのメールを送るにあたり、その本文を取得する。 | |
871 | +# エラーがあった場合はエラーメッセージを送る。 | |
872 | +# | |
873 | +# 戻り値: 下記のように必ずメッセージオブジェクトを返す | |
874 | +# true, msg | |
875 | +# false, msg | |
876 | +# | |
877 | +def get_plugin_body(mail) | |
878 | + di, pname, opts, errmsg = parse_subject_for_plugin!(mail['subject'].dup) | |
879 | + return false, errmsg if errmsg | |
880 | + | |
881 | + # 該当プラグインがあるか? | |
882 | + return false, ERR_PLUGIN_NO_EXIST unless Pmanager.instance.include?(pname) | |
883 | + | |
884 | + # mail_error が Plugin::IGNORE_ME 以外ならば送信メールの本文を得る為に get_mail_body() が実行される。 | |
885 | + plg = nil | |
886 | + body = nil | |
887 | + begin | |
888 | + # プラグインオブジェクトを生成 | |
889 | + plg = eval("Plugin_#{pname}").new(mail, opts, Plugin::MODE_SEND) | |
890 | + | |
891 | + # エラーチェック | |
892 | + case plg.mail_error | |
893 | + # 正常 | |
894 | + when Plugin::NO_ERROR | |
895 | + ; | |
896 | + # エラー | |
897 | + when Plugin::ERROR | |
898 | + errmsg = ERR_PLUGIN_ERROR_MAIL + plg.mail_error_msg | |
899 | + return false, errmsg | |
900 | + # NOTE: 本来、次の無視処理は値が IGNORE_ME のときであるが、 | |
901 | + # その他の値が設定されているときも同様とする | |
902 | + # NOTE: 戻り値として nil を返すことでメールを送信しないようにしている | |
903 | + else | |
904 | + Utils::cputs('info', "ignored: Plugin_#{pname}.mail_error = #{plg.mail_error}") | |
905 | + return true, nil | |
906 | + end | |
907 | + | |
908 | + body = plg.get_mail_body(mail, opts, Plugin::MODE_SEND) | |
909 | + body = body ? NKF.nkf('-j', body) : '' | |
910 | + rescue Exception => e | |
911 | + Utils::cputs('error', "'Plugin_#{pname}' raised in new() or get_mail_body() : #{e.to_s}") | |
912 | + return false, ERR_PLUGIN_EXCEPTION | |
913 | + ensure | |
914 | + begin | |
915 | + plg.delete() if plg != nil | |
916 | + rescue Exception | |
917 | + Utils::cputs('error', "'Plugin_#{pname}' raised in delete()") | |
918 | + end | |
919 | + end | |
920 | + | |
921 | + return true, body | |
922 | +end | |
923 | + | |
924 | + | |
925 | +# plugin メールを保存していいかチェック (MODE_SAVE でチェック) | |
926 | +# OK -> true, nil | |
927 | +# NG -> false, ERRMSG | |
928 | +def check_save_plugin_mail(mail) | |
929 | + di, pname, opts, errmsg = parse_subject_for_plugin!(mail['subject'].dup) | |
930 | + return false, errmsg if errmsg | |
931 | + | |
932 | + # 該当プラグインがあるか? | |
933 | + return false, ERR_PLUGIN_NO_EXIST unless Pmanager.instance.include?(pname) | |
934 | + | |
935 | + # mail_error が Plugin::NO_ERROR 以外ならばエラーとする | |
936 | + plg = nil | |
937 | + begin | |
938 | + # プラグインオブジェクトを生成 | |
939 | + plg = eval("Plugin_#{pname}").new(mail, opts, Plugin::MODE_SAVE) | |
940 | + | |
941 | + # エラーチェック | |
942 | + if plg.mail_error != Plugin::NO_ERROR | |
943 | + errmsg = ERR_PLUGIN_SAVE_MAIL + plg.mail_error_msg | |
944 | + return false, errmsg | |
945 | + end | |
946 | + rescue Exception => e | |
947 | + Utils::cputs('error', "'Plugin_#{pname}' raised in new() : #{e.to_s}") | |
948 | + return false, ERR_PLUGIN_EXCEPTION | |
949 | + ensure | |
950 | + begin | |
951 | + plg.delete() if plg != nil | |
952 | + rescue Exception | |
953 | + Utils::cputs('error', "'Plugin_#{pname}' raised in delete()") | |
954 | + end | |
955 | + end | |
956 | + | |
957 | + return true, nil | |
958 | +end | |
959 | + | |
960 | + | |
961 | +# プラグインの処理 | |
962 | +# OK -> true, nil | |
963 | +# NG -> false, ERRMSG | |
964 | +def process_plugin_mail(sm, mail) | |
965 | + di, pname, opts, errmsg = parse_subject_for_plugin!(mail['subject'].dup) | |
966 | + return false, errmsg if errmsg | |
967 | + | |
968 | + # 該当プラグインがあるか? | |
969 | + return false, ERR_PLUGIN_NO_EXIST unless Pmanager.instance.include?(pname) | |
970 | + | |
971 | + # mail_error が Plugin::IGNORE_ME 以外ならば送信メールの本文を得る為に get_mail_body() が実行される。 | |
972 | + plg = nil | |
973 | + begin | |
974 | + # プラグインオブジェクトを生成 | |
975 | + plg = eval("Plugin_#{pname}").new(mail, opts, Plugin::MODE_SEND) | |
976 | + | |
977 | + # エラーチェック | |
978 | + case plg.mail_error | |
979 | + # 正常 | |
980 | + when Plugin::NO_ERROR | |
981 | + ; | |
982 | + # エラーメール送付 | |
983 | + when Plugin::ERROR | |
984 | + errmsg = ERR_PLUGIN_ERROR_MAIL + plg.mail_error_msg | |
985 | + return false, errmsg | |
986 | + # NOTE: 本来、次の無視処理は値が IGNORE_ME のときであるが、 | |
987 | + # その他の値が設定されているときも同様とする | |
988 | + else | |
989 | + Utils::cputs('info', "ignored: Plugin_#{pname}.mail_error = #{plg.mail_error}") | |
990 | + return true, nil | |
991 | + end | |
992 | + | |
993 | + body = plg.get_mail_body(mail, opts, Plugin::MODE_SEND) | |
994 | + body = body ? NKF.nkf('-j', body) : '' | |
995 | + sm.send_plugin_result(mail, pname, body) | |
996 | + rescue Exception => e | |
997 | + Utils::cputs('error', "'Plugin_#{pname}' raised in new() or get_mail_body() : #{e.to_s}") | |
998 | + return false, ERR_PLUGIN_EXCEPTION | |
999 | + ensure | |
1000 | + begin | |
1001 | + plg.delete() if plg != nil | |
1002 | + rescue Exception | |
1003 | + Utils::cputs('error', "'Plugin_#{pname}' raised in delete()") | |
1004 | + end | |
1005 | + end | |
1006 | + | |
1007 | + return true, nil | |
1008 | +end | |
1009 | + | |
1010 | + | |
1011 | +# リマインダメールの処理 | |
1012 | +# OK -> true, nil | |
1013 | +# NG -> false, ERRMSG | |
1014 | +def process_reminder_mail(sm, mail, time_now) | |
1015 | + # fname はフルパスが入る | |
1016 | + fname, errmsg = parse_subject!(mail['subject'].dup, RESERVED_DIR, time_now) | |
1017 | + return false, errmsg if errmsg | |
1018 | + | |
1019 | + # plugin の場合は SAVE してよいかチェック | |
1020 | + # NOTE: 単純に ':' があるかチェックするだけだと、単なる every シリーズも引っ掛かってしまう | |
1021 | + if mail['subject'] =~ /[0-9]:.*/ | |
1022 | + ret, errmsg = check_save_plugin_mail(mail) | |
1023 | + # NOTE: parse_subject!() 後は fname ファイルを touch で作成済み。よって、戻る場合は削除必須 | |
1024 | + unless ret | |
1025 | + File.unlink(fname) rescue Utils::cputs('error', "Can't unlink a file '#{fname}'") | |
1026 | + return false, errmsg | |
1027 | + end | |
1028 | + end | |
1029 | + | |
1030 | + # 指定時刻にメールを飛ばせるよう、分割したメールを保存 | |
1031 | + save_mail(fname, mail, WORKING_DIR) | |
1032 | + # 受理したことを通知 (ファイル名を stop の引数対象にできるようにする) | |
1033 | + sm.send_accept(fname) | |
1034 | + return true, nil | |
1035 | +end | |
1036 | + | |
1037 | + | |
1038 | +# メールをチェックし、適切な名前を付けたファイルに分割していく | |
1039 | +# mbox が nil ならば何もせずに戻る | |
1040 | +# NOTE: mbox はこの関数内で close するので、呼び側で close しないこと | |
1041 | +def process_incoming_mail(mbox) | |
1042 | + return 0 unless mbox | |
1043 | + | |
1044 | + time_now = Time.now | |
1045 | + | |
1046 | + # メール送信クラスの準備 | |
1047 | + listfiles = ListFiles.new(USER_LIST, ADDME_LIST) | |
1048 | + mailfiles = MailFiles.new(ERR_FILE, HELP_FILE, MAINTE_FILE) | |
1049 | + sinfo = SmtpInfo.new(SMTP_HELO, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_AUTH) | |
1050 | + sm = SendingMail.new(FROM_ADDR, RPLY_ADDR, sinfo, mailfiles, listfiles) | |
1051 | + | |
1052 | + until (m = Mail.new(mbox)).header.empty? | |
1053 | + # 件名がないものは無視 | |
1054 | + unless m['subject'] | |
1055 | + Utils::cputs('debug', "process_incoming_mail(): no subject") | |
1056 | + sm.send_error_mail(m, ERR_NO_SUBJECT) | |
1057 | + next | |
1058 | + end | |
1059 | + | |
1060 | + # 件名をデバッグ情報としてログに出しておく | |
1061 | + if Utils::important_subject?(m['subject']) | |
1062 | + Utils::cputs('warn', "mail having the subject of " + | |
1063 | + "'#{m['subject']}' was recieved, but " + | |
1064 | + "it will be discarded maybe") | |
1065 | + else | |
1066 | + Utils::cputs('debug', "process_incoming_mail(): subject '#{m['subject']}'") | |
1067 | + end | |
1068 | + | |
1069 | + # Content-Type に依らず、許可されていないユーザでも発行する命令 | |
1070 | + case m['subject'] | |
1071 | + # 件名の先頭が addme: ならばホワイトリストへの追加処理 | |
1072 | + when /^addme:/ | |
1073 | + ok, errmsg = process_addme_mail(sm, m) | |
1074 | + # 成功か、失敗によって投げるメールが異なる | |
1075 | + # 失敗の場合、元々ユーザリストに存在するアドレス以外ならば、 | |
1076 | + # メールは実際には飛ばない | |
1077 | + ok ? sm.send_added(m) : sm.send_error_mail(m, errmsg) | |
1078 | + next | |
1079 | + # 件名の先頭が help ならばヘルプメールを送信 | |
1080 | + when /^help/ | |
1081 | + sm.send_help(m) | |
1082 | + next | |
1083 | + end | |
1084 | + | |
1085 | + # | |
1086 | + # 以降、許可されたユーザでなければならない。 | |
1087 | + # | |
1088 | + next unless sm.send_ok?(m) | |
1089 | + | |
1090 | + # Content-Type に依らない命令 | |
1091 | + case m['subject'] | |
1092 | + # 件名の先頭が list ならば自分宛に届くメール一覧を送信 | |
1093 | + when /^list/ | |
1094 | + ok, errmsg = process_list_mail(sm, m) | |
1095 | + sm.send_error_mail(m, errmsg) unless ok | |
1096 | + next | |
1097 | + # 件名の先頭が del:, delete:, stop: ならば指定メールを停止 | |
1098 | + when /^del:/, /^delete:/, /^stop:/ | |
1099 | + ok, errmsg = process_stop_mail(sm, m) | |
1100 | + sm.send_error_mail(m, errmsg) unless ok | |
1101 | + next | |
1102 | + # メモリスト | |
1103 | + when /^memolist/, /^ml/ | |
1104 | + ok, errmsg = process_memolist_mail(sm, m) | |
1105 | + sm.send_error_mail(m, errmsg) unless ok | |
1106 | + next | |
1107 | + # バージョン情報 | |
1108 | + # NOTE: 許可されたユーザのみに発行する | |
1109 | + when /^version/, /^vr/ | |
1110 | + ok, errmsg = process_version(sm, m) | |
1111 | + sm.send_error_mail(m, errmsg) unless ok | |
1112 | + next | |
1113 | + end | |
1114 | + | |
1115 | + # | |
1116 | + # 以降、Content-Type が text/plain でないものはエラーとする | |
1117 | + # | |
1118 | + if m['content-type'] and m['content-type'].index("text/plain") == nil | |
1119 | + sm.send_error_mail(m, ERR_CONTENT_TYPE) | |
1120 | + next | |
1121 | + end | |
1122 | + | |
1123 | + # メンテナンス中は分割保存しないので、周知メールを出しておく | |
1124 | + if NOW_MAINTENANCE | |
1125 | + sm.send_maintenance(m) | |
1126 | + next | |
1127 | + end | |
1128 | + | |
1129 | + case m['subject'] | |
1130 | + # メモ設置 | |
1131 | + when /^memoput:/, /^mp:/ | |
1132 | + ok, errmsg = process_memoput_mail(sm, m) | |
1133 | + # メモ取得 | |
1134 | + when /^memoget:/, /^mg:/ | |
1135 | + ok, errmsg = process_memoget_mail(sm, m) | |
1136 | + # 即時返答を行うプラグイン | |
1137 | + when /^plugin:/, /^plg:/ | |
1138 | + ok, errmsg = process_plugin_mail(sm, m) | |
1139 | + # 予約など (プラグイン処理も含む) | |
1140 | + else | |
1141 | + ok, errmsg = process_reminder_mail(sm, m, time_now) | |
1142 | + end | |
1143 | + sm.send_error_mail(m, errmsg) unless ok | |
1144 | + end | |
1145 | + | |
1146 | + # 後始末 | |
1147 | + mbox.close | |
1148 | + sm.sfree # SMTP のセッション解放は必須 | |
1149 | + sm.write_back_addme() # addme の追加分書き出し | |
1150 | + | |
1151 | + return 0 | |
1152 | +end | |
1153 | + | |
1154 | + | |
1155 | +# 渡された Time オブジェクトを使用し、 | |
1156 | +# 基本的なメールファイル名配列を返す | |
1157 | +def make_time_filenames(tm) | |
1158 | + return [ | |
1159 | + tm.strftime('HOUR_%M_'), | |
1160 | + tm.strftime('DAY_%H%M_'), | |
1161 | + tm.strftime('WEEK_%w_%H%M_'), | |
1162 | + tm.strftime('MONTH_%d_%H%M_'), | |
1163 | + tm.strftime('YEAR_%m%d_%H%M_'), | |
1164 | + tm.strftime('%Y%m%d_%H%M_') | |
1165 | + ] | |
1166 | +end | |
1167 | + | |
1168 | + | |
1169 | +# 指定されたメールファイルを送信する | |
1170 | +# fpath はフルパスであること | |
1171 | +# 送信が終わり次第、そのファイルは削除される | |
1172 | +def send_mail_file(fpath, sendmail, clean) | |
1173 | + # 存在しない、またはサイズが 0 だと nil | |
1174 | + return false unless FileTest.size?(fpath) | |
1175 | + | |
1176 | + # メールの送信 | |
1177 | + begin | |
1178 | + f = File.open(fpath, "r+") # r'+' はロックのため | |
1179 | + raise unless f.flock(File::LOCK_EX | File::LOCK_NB) == 0 | |
1180 | + m = Mail.new(f) | |
1181 | + | |
1182 | + # プラグインチェックとメール送信 | |
1183 | + if m['subject'] =~ /[0-9]:.*/ | |
1184 | + ok, plg_body = get_plugin_body(m) | |
1185 | + # plg_body が nil ではない限り、とりあえずメールを送る | |
1186 | + # (plugin の結果ではなく、エラーメッセージの可能性がある) | |
1187 | + sendmail.send_time_mail(m, fpath, clean, plg_body) if plg_body != nil | |
1188 | + else | |
1189 | + sendmail.send_time_mail(m, fpath, clean) | |
1190 | + end | |
1191 | + | |
1192 | + # このロック外しの結果は見なくてもいいだろう | |
1193 | + f.flock(File::LOCK_UN) | |
1194 | + f.close | |
1195 | + f = nil # 余計な close が起こらないようにしておく | |
1196 | + rescue | |
1197 | + Utils::cputs('warn', "Can't process #{fpath}") | |
1198 | + f.close if f | |
1199 | + return false | |
1200 | + end | |
1201 | + | |
1202 | + # もう要らなくなったファイルを削除 | |
1203 | + begin | |
1204 | + # ファイル名先頭が数値ならば every シリーズではない | |
1205 | + # そして、それは一回限りの送信として扱うために削除 | |
1206 | + File.unlink(fpath) if File.basename(fpath).index(/\d/) == 0 | |
1207 | + return true | |
1208 | + rescue | |
1209 | + Utils::cputs('warn', "Can't unlink #{fpath}") | |
1210 | + return false | |
1211 | + end | |
1212 | + | |
1213 | + raise 'Control never reach: send_mail_file' | |
1214 | +end | |
1215 | + | |
1216 | + | |
1217 | +# 指定時刻のメールを送信する | |
1218 | +# 無条件に 0 を返すことにしておく | |
1219 | +# (大抵は送信数が 0 だと思うので) | |
1220 | +def mode_send(tm) | |
1221 | + # メール送信クラスの準備 | |
1222 | + listfiles = ListFiles.new(USER_LIST, ADDME_LIST) | |
1223 | + mailfiles = MailFiles.new(ERR_FILE, HELP_FILE, MAINTE_FILE) | |
1224 | + sinfo = SmtpInfo.new(SMTP_HELO, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_AUTH) | |
1225 | + sm = SendingMail.new(FROM_ADDR, RPLY_ADDR, sinfo, mailfiles, listfiles) | |
1226 | + | |
1227 | + sum = 0 | |
1228 | + make_time_filenames(tm).each do |bname| | |
1229 | + path = RESERVED_DIR + '/' + bname + '*' | |
1230 | + # ディレクトリから該当するファイル名を取得する | |
1231 | + Dir.glob(path).each do |fname| | |
1232 | + # 無事に送信できたならば送信数を増やしておく | |
1233 | + sum += 1 if send_mail_file(fname, sm, false) | |
1234 | + end | |
1235 | + end | |
1236 | + | |
1237 | + # 後始末 | |
1238 | + sm.sfree # SMTP のセッション解放は必須 | |
1239 | + | |
1240 | + return 0 | |
1241 | +end | |
1242 | + | |
1243 | + | |
1244 | +# チェックを行うが、デーモンになるときは実行ユーザが変更となるため、 | |
1245 | +# データファイル及びディレクトリの確認は別途行うことにする | |
1246 | +def main_check() | |
1247 | + # 引数の数が合っていれば何もしない | |
1248 | + # モードもチェックする | |
1249 | + ret = usage(ARGV.length) | |
1250 | + return ret if ret != 0 | |
1251 | + | |
1252 | + # モード以外の引数をチェック | |
1253 | + ret = check_args() | |
1254 | + return ret if ret != 0 | |
1255 | + | |
1256 | + # デーモンモード、または hup ならば戻る | |
1257 | + return 0 if DAEMON_MODES.index(ARGV[MODE]) | |
1258 | + return 0 if 'hup' == ARGV[MODE] | |
1259 | + | |
1260 | + | |
1261 | + # データディレクトリの確認 | |
1262 | + ret = check_and_mkdir_wrap() | |
1263 | + return ret if ret != 0 | |
1264 | + | |
1265 | + # plugin の読込み | |
1266 | + ret = init_plugin() | |
1267 | + return ret if ret != 0 | |
1268 | + | |
1269 | + # ユーザリストの確認 | |
1270 | + ret = check_userlist(USER_LIST, USER_LIST_TEMPLATE) | |
1271 | + return ret if ret != 0 | |
1272 | + | |
1273 | + # 脆弱性に繋がる設定は許さない | |
1274 | + # 引数とか考えるのが面倒なので、関数内で広域変数をそのまま参照する | |
1275 | + ret = check_setting() | |
1276 | + return ret if ret != 0 | |
1277 | + | |
1278 | + return 0 | |
1279 | +end | |
1280 | + | |
1281 | + | |
1282 | +# デーモンとなった後で行う各種チェック | |
1283 | +# NOTE: この関数はユーザ D_USER_NAME で処理されるべき | |
1284 | +def daemon_check_by_switched_user() | |
1285 | + # データディレクトリの確認 | |
1286 | + ret = check_and_mkdir_wrap() | |
1287 | + return ret if ret != 0 | |
1288 | + | |
1289 | + # plugin の読込み | |
1290 | + ret = init_plugin() | |
1291 | + return ret if ret != 0 | |
1292 | + | |
1293 | + # ユーザリストの確認 | |
1294 | + ret = check_userlist(USER_LIST, USER_LIST_TEMPLATE) | |
1295 | + return ret if ret != 0 | |
1296 | + | |
1297 | + # 脆弱性に繋がる設定は許さない | |
1298 | + # 引数とか考えるのが面倒なので、関数内で広域変数をそのまま参照する | |
1299 | + ret = check_setting() | |
1300 | + return ret if ret != 0 | |
1301 | + | |
1302 | + return 0 | |
1303 | +end | |
1304 | + | |
1305 | + | |
1306 | +# POP3 でチェック中のメールがこのシステム宛かどうかを判定する | |
1307 | +def is_mail_to_system(popmail) | |
1308 | + hs = popmail.header.split("\n") | |
1309 | + return false if hs.empty? | |
1310 | + | |
1311 | + # each にしないのは、次行を直接覗きたい場面があるため | |
1312 | + for i in 0 .. hs.length - 1 | |
1313 | + # To フィールドでなければ次行へ | |
1314 | + # 大抵は To だと思うが、大文字小文字どちらも受け入れるのが正しいはず | |
1315 | + next unless hs[i].sub!(/^to: (.*)$/i, '\1') | |
1316 | + | |
1317 | + # 指定の文字列があれば、システム宛てと認識する | |
1318 | + return true if hs[i].index(POP3_TO_ADDR) | |
1319 | + # 次行に続く場合があるので、それに対応 | |
1320 | + loop { | |
1321 | + i += 1 | |
1322 | + return false if hs[i].index(' ') != 0 | |
1323 | + return true if hs[i].index(POP3_TO_ADDR) | |
1324 | + } | |
1325 | + end | |
1326 | + return false | |
1327 | +end | |
1328 | + | |
1329 | + | |
1330 | +# POP3 サーバからシステム宛のメールを取得し、 | |
1331 | +# ファイルに保存する。 | |
1332 | +# 戻り値はそのファイルハンドル。 | |
1333 | +# 何もメールがなかったときは nil を返す | |
1334 | +# | |
1335 | +# 引数: | |
1336 | +# fname 取得したメールを格納するファイル名 | |
1337 | +def get_mail_by_pop3(fname) | |
1338 | + ret = nil | |
1339 | + # POP3_APOP が true ならば Net:APOP クラスが返る | |
1340 | + pop_apop = Net::POP3.APOP(POP3_APOP).new(POP3_HOST, POP3_PORT) | |
1341 | + pop_apop.start(POP3_USER, POP3_PASS) { |session| | |
1342 | + # メールが無いときは戻る | |
1343 | + return nil if session.mails.empty? | |
1344 | + | |
1345 | + # POP3 サーバ上のメールを走査する | |
1346 | + f = nil | |
1347 | + session.each_mail do |m| | |
1348 | + # このシステム宛てでなければ次へ | |
1349 | + next unless is_mail_to_system(m) | |
1350 | + | |
1351 | + # このシステム宛てなのでファイルへ書き込み | |
1352 | + f = File.open(fname, "w") unless f | |
1353 | + f.write m.pop | |
1354 | + m.delete | |
1355 | + end | |
1356 | + | |
1357 | + # 後始末と戻り値代入 | |
1358 | + if f | |
1359 | + f.close | |
1360 | + ret = File.open(fname) | |
1361 | + end | |
1362 | + } | |
1363 | + return ret | |
1364 | +end | |
1365 | + | |
1366 | + | |
1367 | +# 指定時刻に送信されなかったメールをこのタイミングで送信し、 | |
1368 | +# その後削除する。 | |
1369 | +# NOTE: 例えば、当時電源が落ちていた、などで未送信のメールは出てくるはず | |
1370 | +def mode_clean() | |
1371 | + # 一回限りのメールを拾い上げる | |
1372 | + files = Dir.glob(RESERVED_DIR + "/[0-9]*") | |
1373 | + return 0 if files.empty? | |
1374 | + | |
1375 | + # メール送信クラスの準備 | |
1376 | + listfiles = ListFiles.new(USER_LIST, ADDME_LIST) | |
1377 | + mailfiles = MailFiles.new(ERR_FILE, HELP_FILE, MAINTE_FILE) | |
1378 | + sinfo = SmtpInfo.new(SMTP_HELO, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_AUTH) | |
1379 | + sm = SendingMail.new(FROM_ADDR, RPLY_ADDR, sinfo, mailfiles, listfiles) | |
1380 | + | |
1381 | + files.each do |fpath| | |
1382 | + name = File.basename(fpath) | |
1383 | + # 一分前よりも古ければメール送信 | |
1384 | + if is_old_filename(name, Time.now, (-1 * 60)) | |
1385 | + send_mail_file(fpath, sm, true) | |
1386 | + end | |
1387 | + end | |
1388 | + | |
1389 | + # 後始末 | |
1390 | + sm.sfree # SMTP のセッション解放は必須 | |
1391 | + | |
1392 | + return 0 | |
1393 | +end | |
1394 | + | |
1395 | + | |
1396 | +# mbox モードはファイルをテンポラリ化したあとに開く | |
1397 | +# TODO: 意図的に攻撃することで mbox モードのテンポラリファイルは上書きされる可能性がある | |
1398 | +def mode_mbox() | |
1399 | + mbox_temp = Dir.tmpdir + "/mbox_" + Time.now.strftime('%Y%m%d%H%M%S') + $$.to_s | |
1400 | + | |
1401 | + mbox = move_and_open(ARGV[MBOX], mbox_temp) | |
1402 | + ret = process_incoming_mail(mbox) | |
1403 | + return ret unless mbox | |
1404 | + | |
1405 | + File.unlink(mbox_temp) rescue ret += 62 | |
1406 | + return ret | |
1407 | +end | |
1408 | + | |
1409 | + | |
1410 | +# pop3 モードは先に自分宛てのメールを取得し、 | |
1411 | +# その後にメールの解析作業を行う | |
1412 | +# TODO: mbox モードと処理構造が被っているが、今度同じのが増えたら考える | |
1413 | +# TODO: 意図的に攻撃することで pop3 モードのテンポラリファイルは上書きされる可能性がある | |
1414 | +def mode_pop3() | |
1415 | + pop3_temp = Dir.tmpdir + "/pop3_" + Time.now.strftime('%Y%m%d%H%M%S') + $$.to_s | |
1416 | + | |
1417 | + mbox = get_mail_by_pop3(pop3_temp) | |
1418 | + ret = process_incoming_mail(mbox) | |
1419 | + return ret unless mbox | |
1420 | + | |
1421 | + File.unlink(pop3_temp) rescue ret += 60 | |
1422 | + return ret | |
1423 | +end | |
1424 | + | |
1425 | + | |
1426 | +# pid ファイルを残しておく | |
1427 | +def write_pid_file(pid_file, pid, cmd_str) | |
1428 | + ret = 0 | |
1429 | + fname = pid_file ? pid_file : 'nil' | |
1430 | + s = cmd_str ? cmd_str : 'nil' | |
1431 | + Utils::cputs('debug', "pid_file=#{fname}, pid=#{pid}, cmd_str=#{s}") | |
1432 | + | |
1433 | + begin | |
1434 | + f = File.open(pid_file, 'w') | |
1435 | + f.puts pid | |
1436 | + f.puts cmd_str if cmd_str | |
1437 | + rescue | |
1438 | + Utils::cputs('warn', "Can't write pid file: pid_file=#{fname}, " + | |
1439 | + "pid=#{pid}, cmd_str=#{s}") | |
1440 | + ret = 70 | |
1441 | + ensure | |
1442 | + f.close if f | |
1443 | + end | |
1444 | + | |
1445 | + return ret | |
1446 | +end | |
1447 | + | |
1448 | + | |
1449 | +# utils.rb の utils_log を初期化する | |
1450 | +def init_logger() | |
1451 | + fpath = LOG_DIR + '/simrm.log' | |
1452 | + $utils_log = Logger.new(fpath, 'daily') | |
1453 | + $utils_log.level = D_LOG_LEVEL | |
1454 | +end | |
1455 | + | |
1456 | + | |
1457 | +# デーモンのメイン処理 | |
1458 | +# | |
1459 | +# NOTE: デーモンのメインスレッドは 1 秒ごとに処理を行うが、 | |
1460 | +# 基本的に時間の掛かる処理は行わないこと。 | |
1461 | +# スケジューラに専念する。 | |
1462 | +def daemon_process(f_receive, d_receive) | |
1463 | + # スーパーユーザ権限を捨てる | |
1464 | + Utils::switch_uid(D_USER_NAME) | |
1465 | + | |
1466 | + # もう stdout, stderr が使えないので Logger オブジェクトを使う | |
1467 | + init_logger() | |
1468 | + Utils::cputs('debug', "Daemon and logger started!!") | |
1469 | + | |
1470 | + # シグナルを trap しておく | |
1471 | + is_signal = false | |
1472 | + trap("SIGINT") { is_signal = true; Utils::cputs('debug', "Caught 'SIGINT'") } | |
1473 | + trap("SIGTERM"){ is_signal = true; Utils::cputs('debug', "Caught 'SIGTERM'") } | |
1474 | + trap("SIGHUP") { is_signal = true; Utils::cputs('debug', "Caught 'SIGHUP'") } | |
1475 | + | |
1476 | + # 初回は必ずそれぞれのモードが実行されるようにする | |
1477 | + old_send_sec = 61 | |
1478 | + old_clean_time = Time.local(2000) | |
1479 | + old_receive_time = Time.local(2000) | |
1480 | + | |
1481 | + loop { | |
1482 | + t = Time.new | |
1483 | + if t.sec < old_send_sec | |
1484 | + old_send_sec = t.sec + 1 | |
1485 | + Thread.new { | |
1486 | + begin | |
1487 | + mode_send(Time.now) | |
1488 | + rescue Exception => e | |
1489 | + Utils::cputs('error', "mode_send(): raise: #{e.to_s}") | |
1490 | + end | |
1491 | + } | |
1492 | + end | |
1493 | + if t - old_clean_time >= D_CLEAN_INTERVAL | |
1494 | + old_clean_time = t | |
1495 | + Thread.new { | |
1496 | + begin | |
1497 | + mode_clean() | |
1498 | + rescue Exception => e | |
1499 | + Utils::cputs('error', "mode_clean(): raise: #{e.to_s}") | |
1500 | + end | |
1501 | + } | |
1502 | + end | |
1503 | + if t - old_receive_time >= d_receive | |
1504 | + old_receive_time = t | |
1505 | + Thread.new { | |
1506 | + begin | |
1507 | + f_receive.call() | |
1508 | + rescue Exception => e | |
1509 | + Utils::cputs('error', "f_receive.call(): raise: #{e.to_s}") | |
1510 | + end | |
1511 | + } | |
1512 | + end | |
1513 | + | |
1514 | + # シグナルを受信していないかチェック | |
1515 | + # NOTE: 他スレッドの終わりを待ち、その後、終了する | |
1516 | + if is_signal | |
1517 | + Thread.list.each { |th| | |
1518 | + th.join if th != Thread.main | |
1519 | + } | |
1520 | + Utils::cputs('debug', "Other threads exited, and main thread will be end, too") | |
1521 | + Utils::cputs('info', "BYE!") | |
1522 | + exit 0 | |
1523 | + end | |
1524 | + | |
1525 | + sleep 1 | |
1526 | + } | |
1527 | +end | |
1528 | + | |
1529 | + | |
1530 | +# デーモン化 | |
1531 | +# mode: | |
1532 | +# nil -> メールの取得作業はしない (pukiwiki 運用時など) | |
1533 | +# 'mbox' -> メールの取得は mbox モードで | |
1534 | +# 'pop3' -> メールの取得は pop3 モードで | |
1535 | +# | |
1536 | +# See: note/daemon_thread.txt | |
1537 | +def mode_daemon(mode) | |
1538 | + | |
1539 | + # メソッドオブジェクトを使い、処理を一本化する | |
1540 | + begin | |
1541 | + case mode | |
1542 | + when nil | |
1543 | + include Utils # for do_nothing | |
1544 | + receive = method(:do_nothing) | |
1545 | + # 定期的に受信メールをチェックする必要がないため適当な値でよい | |
1546 | + d_receive_interval = 60 * 60 * 60 | |
1547 | + when 'mbox' | |
1548 | + receive = method(:mode_mbox) | |
1549 | + d_receive_interval = D_MBOX_INTERVAL | |
1550 | + when 'pop3' | |
1551 | + receive = method(:mode_pop3) | |
1552 | + d_receive_interval = D_POP3_INTERVAL | |
1553 | + else | |
1554 | + Utils::cputs('fatal', "'#{mode}' mode is invalid") | |
1555 | + return 110 | |
1556 | + end | |
1557 | + rescue | |
1558 | + Utils::cputs('fatal', "method() occurs exception") | |
1559 | + return 111 | |
1560 | + end | |
1561 | + | |
1562 | + # pid ファイルの書き込みチェック (まだ実際には書き込まない) | |
1563 | + begin | |
1564 | + f = File.open(D_PID_FILE, 'a') | |
1565 | + f.close | |
1566 | + rescue | |
1567 | + Utils::cputs('error', "Can't write pid file #{D_PID_FILE}") | |
1568 | + return 112 | |
1569 | + end | |
1570 | + | |
1571 | + # ファイルやディレクトリのチェック | |
1572 | + ret = Utils::switch_euid(D_USER_NAME, true) { | |
1573 | + daemon_check_by_switched_user() | |
1574 | + } | |
1575 | + return ret if ret != 0 | |
1576 | + | |
1577 | + # デーモン化 | |
1578 | + cpid = Utils::simple_daemon() { | |
1579 | + daemon_process(receive, d_receive_interval) | |
1580 | + } | |
1581 | + # NOTE: 以降は親プロセスのみの処理となる | |
1582 | + | |
1583 | + # 子プロセス番号を知ったので、pid ファイルを生成して終了 | |
1584 | + ret = write_pid_file(D_PID_FILE, cpid, "#{ARGV.join(' ')}") | |
1585 | + exit! ret | |
1586 | +end | |
1587 | + | |
1588 | + | |
1589 | +# kill 処理 | |
1590 | +# NOTE: 自身が root ならばユーザを変更すること | |
1591 | +# See: note/kill_hup.txt | |
1592 | +def kill_proc(pid) | |
1593 | + ret = Utils::switch_euid(D_USER_NAME, true) { | |
1594 | + Process.kill('SIGTERM', pid) | |
1595 | + } | |
1596 | + if ret != 1 | |
1597 | + Utils::cputs('error', "Can't kill pid=#{pid}") | |
1598 | + return 90 | |
1599 | + end | |
1600 | + | |
1601 | + return 0 | |
1602 | +end | |
1603 | + | |
1604 | + | |
1605 | +# pid ファイル読み込み処理 | |
1606 | +# 戻り値: [pid, args] | |
1607 | +# See: note/kill_hup.txt | |
1608 | +def read_pid_file(fpath) | |
1609 | + begin | |
1610 | + f = File.open(fpath) | |
1611 | + pid = f.gets.chomp!.to_i | |
1612 | + args = f.gets.chomp | |
1613 | + return [pid, args] | |
1614 | + rescue | |
1615 | + Utils::cputs('error', "Can't read pidfile=#{fpath}") | |
1616 | + raise | |
1617 | + ensure | |
1618 | + f.close if f | |
1619 | + end | |
1620 | +end | |
1621 | + | |
1622 | + | |
1623 | +# pid ファイルを読み込み、該当プロセスを kill する | |
1624 | +# See: note/kill_hup.txt | |
1625 | +def mode_kill() | |
1626 | + pid, args = read_pid_file(D_PID_FILE) | |
1627 | + return kill_proc(pid) | |
1628 | +end | |
1629 | + | |
1630 | + | |
1631 | +# pid ファイルを読み込み、該当プロセスを kill する。 | |
1632 | +# その後、引数が同じコマンドを起動する。 | |
1633 | +# | |
1634 | +# NOTE: pid ファイルが無い場合は例外で異常終了 | |
1635 | +# NOTE: pid ファイルはあるが kill に失敗した場合、新規にプロセスを立ち上げる | |
1636 | +# | |
1637 | +# See: note/kill_hup.txt | |
1638 | +def mode_hup() | |
1639 | + pid, args = read_pid_file(D_PID_FILE) | |
1640 | + | |
1641 | + # kill に失敗しても新しいプロセスは立ち上げる | |
1642 | + begin | |
1643 | + ret = kill_proc(pid) | |
1644 | + return ret if ret != 0 | |
1645 | + rescue | |
1646 | + Utils::cputs('info', "Can't kill process #{pid}") | |
1647 | + Utils::cputs('info', "But, continue booting #{$0} ...") | |
1648 | + end | |
1649 | + | |
1650 | + # TODO: system への引数渡しをもっと簡潔にできないものか? | |
1651 | + arg1, arg2 = args.split(' ') | |
1652 | + if !arg1 then ret = nil | |
1653 | + elsif !arg2 then ret = system($0, arg1) | |
1654 | + else ret = system($0, arg1, arg2) | |
1655 | + end | |
1656 | + | |
1657 | + if !ret | |
1658 | + Utils::cputs('error', "Can't reboot #{$0}") | |
1659 | + ret = 50 | |
1660 | + elsif $?.to_i / 256 != 0 | |
1661 | + Utils::cputs('error', "#{$0} returns #{$?.to_i / 256}") | |
1662 | + ret = 51 | |
1663 | + else | |
1664 | + ret = 0 | |
1665 | + end | |
1666 | + | |
1667 | + return ret | |
1668 | +end | |
1669 | + | |
1670 | + | |
1671 | +################################################# | |
1672 | +# main | |
1673 | +################################################# | |
1674 | +def main() | |
1675 | + time_now = Time.now | |
1676 | + | |
1677 | + # 念のため、umask を変更しておく | |
1678 | + File.umask(UMASK) | |
1679 | + | |
1680 | + # チェック各種 | |
1681 | + ret = main_check() | |
1682 | + return ret if ret != 0 | |
1683 | + | |
1684 | + case ARGV[MODE] | |
1685 | + # clean モード: 指定時刻に send できなかったメールを処理する | |
1686 | + when 'clean' then mode_clean() | |
1687 | + when 'dcs' then mode_daemon(nil) | |
1688 | + when 'dcsm' then mode_daemon('mbox') | |
1689 | + when 'dcsp' then mode_daemon('pop3') | |
1690 | + when 'hup' then mode_hup() | |
1691 | + when 'kill' then mode_kill() | |
1692 | + when 'mbox' then mode_mbox() | |
1693 | + when 'pop3' then mode_pop3() | |
1694 | + when 'send' then mode_send(time_now) | |
1695 | + # sendmail 等の aliases で標準入力を受けたり、 | |
1696 | + # デバッグ時にテストファイルを cat して流し込むために使用 | |
1697 | + when 'stdin' then process_incoming_mail(STDIN) | |
1698 | + else | |
1699 | + raise 'Control never reach.' | |
1700 | + end | |
1701 | + | |
1702 | + return 0 | |
1703 | +end | |
1704 | + | |
1705 | + | |
1706 | +# main の実行 | |
1707 | +if $0 == __FILE__ | |
1708 | + exit main() | |
1709 | +end | |
1710 | + | |
1711 | +__END__ |
@@ -0,0 +1,259 @@ | ||
1 | +<?php | |
2 | +/** | |
3 | + * SimRM plugin for PukiWiki | |
4 | + * $Id$ | |
5 | + */ | |
6 | +require_once (LIB_DIR . 'mimeDecode.php'); | |
7 | + | |
8 | +/** | |
9 | + * SimRM の実行パスとヘルプテキストを設定してください | |
10 | + */ | |
11 | +define('SIMRM_EXE', '/var/www/html/8080/wiki/simrm/bin/simrm.rb'); | |
12 | + | |
13 | +/** | |
14 | + * SimRM の DATADIR を設定してください | |
15 | + */ | |
16 | +define('DATADIR', '/var/www/html/8080/wiki/simrm'); | |
17 | +define('RESERVED_DIR', DATADIR . '/reserve'); | |
18 | + | |
19 | +/** | |
20 | + * wiki の文字エンコーディング | |
21 | + * mb_convert_encoding() が使えなさすぎなので、iconv を使用。 | |
22 | + * よって、HTML-ENTITIES は使用できない。文字コードを正しく設定すること。 | |
23 | + */ | |
24 | +define('WIKI_ENCODE', 'EUC-JP'); // ここを変更すること | |
25 | +define('MAIL_ENCODE', 'ISO-2022-JP'); // メールは iso-2022-jp 固定 | |
26 | + | |
27 | + | |
28 | +// ------------------------------------------------------------------ | |
29 | + | |
30 | +/** | |
31 | + * ヘッダを参照し、宛先を得る。 | |
32 | + * Reply-To, Sender, From の順。 | |
33 | + */ | |
34 | +function resolv_to_address($headers) | |
35 | +{ | |
36 | + if ($headers['Reply-to']) return $headers['Reply-to']; | |
37 | + if ($headers['Sender']) return $headers['Sender']; | |
38 | + if ($headers['From']) return $headers['From']; | |
39 | + return ''; | |
40 | +} | |
41 | + | |
42 | + | |
43 | +/** | |
44 | + * ディレクトリから既存のメールファイルを読み出し、 | |
45 | + * 簡易情報のテーブルを返す。 | |
46 | + */ | |
47 | +function make_reserved_str() | |
48 | +{ | |
49 | + // ディレクトリのエラーチェック | |
50 | + if (!is_dir(RESERVED_DIR) || !is_readable(RESERVED_DIR)) { | |
51 | + return '<table border="1" align="center" width="98%">' | |
52 | + . '<tr><td>ERROR: ' . RESERVED_DIR . ' is not readable.</td></tr></table>'; | |
53 | + } | |
54 | + | |
55 | + $ret = '<table border="1" align="center" width="98%">'; | |
56 | + $items = ''; | |
57 | + foreach (glob(RESERVED_DIR . '/*') as $fname) { | |
58 | + // ファイルでなければ次へ | |
59 | + if (!is_file($fname)) { | |
60 | + continue; | |
61 | + } | |
62 | + | |
63 | + $bname = basename($fname); | |
64 | + $items .= '<tr><td>' . $bname . '</td>'; | |
65 | + if (!is_readable($fname)) { | |
66 | + $items .= "<td>ERROR: This file is not readable.</td></tr>\n"; | |
67 | + continue; | |
68 | + } | |
69 | + | |
70 | + // メールファイルをヘッダ、本文に分割する | |
71 | + $mail = file_get_contents($fname); | |
72 | + $decode = new Mail_mimeDecode($mail); | |
73 | + $decode->decode(); | |
74 | + $parts = $decode->getSendArray(); | |
75 | + if (PEAR::isError($parts)) { | |
76 | + $items .= "<td>ERROR: getSendArray()</td></tr>\n"; | |
77 | + continue; | |
78 | + } | |
79 | + list($recipients, $headers, $body) = $parts; | |
80 | + | |
81 | + // ヘッダから宛先を取得する | |
82 | + $h = resolv_to_address($headers); | |
83 | + $h = iconv(MAIL_ENCODE, WIKI_ENCODE, $h); | |
84 | + $h = htmlentities($h, ENT_QUOTES, WIKI_ENCODE); | |
85 | + $items .= '<td>' . $h . '</td>'; | |
86 | + | |
87 | + // 本文を HTML に表示可能な文字列にする | |
88 | + $b = iconv(MAIL_ENCODE, WIKI_ENCODE, $body); | |
89 | + $b = htmlentities($b, ENT_QUOTES, WIKI_ENCODE); | |
90 | + $b = preg_replace('/\r/', '', $b); | |
91 | + $b = preg_replace('/\n/', '<br/>', $b); | |
92 | + $items .= '<td>' . $b . "</td></tr>\n"; | |
93 | + | |
94 | + } | |
95 | + | |
96 | + /* アイテムが 1 つも無い場合はその旨を表示 */ | |
97 | + if (strcmp($items, '') == 0) { | |
98 | + $items .= '<tr><td>NO ITEM</td></tr>'; | |
99 | + } | |
100 | + else { | |
101 | + $items = "<tr><td>ファイル名</td><td>宛先</td><td>本文</td></tr>\n" | |
102 | + . $items; | |
103 | + } | |
104 | + return '<table border="1" align="center" width="98%">' . "\n" . | |
105 | + $items . "</table>\n"; | |
106 | +} | |
107 | + | |
108 | + | |
109 | +/** | |
110 | + * POST データのエラーチェック | |
111 | + */ | |
112 | +function check_post_data() | |
113 | +{ | |
114 | + global $post; | |
115 | + if (strpos($post['to'], '@') === FALSE) { | |
116 | + return '<p>宛先にアドレスがありません。</p>'; | |
117 | + } | |
118 | + return ''; | |
119 | +} | |
120 | + | |
121 | + | |
122 | +/** | |
123 | + * simrm.rb を実行し、その標準入力に文字列を流し込む関数。 | |
124 | + * エラー時はエラー文字列オブジェクトを返す | |
125 | + */ | |
126 | +function exec_simrm($in_str, $oldurl) | |
127 | +{ | |
128 | + if (! $p = popen(SIMRM_EXE . ' stdin', "w")) { | |
129 | + $body = '<p>' . SIMRM_EXE . ' の実行に失敗しました。</p>' . "\n" | |
130 | + . '<p><a href="' . $oldurl . '">前のページに戻る</a></p>' . "\n"; | |
131 | + return $body; | |
132 | + } | |
133 | + if (fwrite($p, $in_str) === FALSE) { | |
134 | + $body = '<p>' . SIMRM_EXE . ' へのデータ渡しに失敗しました。</p>' . "\n" | |
135 | + . '<p><a href="' . $oldurl . '">前のページに戻る</a></p>' . "\n"; | |
136 | + return $body; | |
137 | + } | |
138 | + $status = pclose($p); | |
139 | + if ($status != 0) { | |
140 | + $body = '<p>' . SIMRM_EXE . ' は終了ステータス ' | |
141 | + . $status . ' を返しました。<br/>' . "\n" | |
142 | + . '詳しくは HTTP サーバのエラーログなどを参照してください。</p>' . "\n" | |
143 | + . '<p><a href="' . $oldurl . '">前のページに戻る</a></p>' . "\n"; | |
144 | + return $body; | |
145 | + } | |
146 | + | |
147 | + // 以下、正常終了時 | |
148 | + return NULL; | |
149 | +} | |
150 | + | |
151 | + | |
152 | +/** | |
153 | + * POST 時に呼ばれる関数。 | |
154 | + * POST された内容をチェックし、simrm へ内容を渡す。 | |
155 | + */ | |
156 | +function plugin_simrm_action() | |
157 | +{ | |
158 | + global $post; | |
159 | + | |
160 | + // 身元が疑わしいときはトップページへ戻す | |
161 | + if ($post['plugin'] != 'simrm') return FALSE; | |
162 | + if ($post['mode'] != 'submit') return FALSE; | |
163 | + if (strlen($post['oldurl']) < 5) return FALSE; | |
164 | + | |
165 | + // Wiki のヘッダメッセージと本文 | |
166 | + $msg = ''; | |
167 | + $body = '<a href="' . $post['oldurl'] . '">前のページに戻る</a>'; | |
168 | + | |
169 | + // エラーチェック | |
170 | + $err = check_post_data(); | |
171 | + if ($err != '') { | |
172 | + $msg = 'Error of SimRM'; | |
173 | + $body .= $err; | |
174 | + $body .= '<p><a href="' . $post['oldurl'] . '">前のページに戻る</a></p>'; | |
175 | + return (array('msg' => $msg, 'body' => $body)); | |
176 | + } | |
177 | + | |
178 | + // SimRM に渡すメール内容は適切なエンコーディングを施す | |
179 | + // TODO: To, Subject フィールドには MIME エンコードが必要なはず | |
180 | + // TODO: To, Subject フィールドは改行を削除しなくてはいけない | |
181 | + $jto = iconv(WIKI_ENCODE, MAIL_ENCODE, $post['to']); | |
182 | + $jsub = iconv(WIKI_ENCODE, MAIL_ENCODE, $post['subject']); | |
183 | + // NOTE: 本文各行先頭の From は > を付ける (Thunderbird に倣った) | |
184 | + $jbody = preg_replace("/^(from\s)/mi", ">\\1", $post['body']); | |
185 | + $jbody = iconv(WIKI_ENCODE, MAIL_ENCODE, $jbody); | |
186 | + | |
187 | + // NOTE: From: に $post['to'] の内容があるのは間違いではない | |
188 | + $mail_str = <<<M_END | |
189 | +To: SimRM | |
190 | +From: $jto | |
191 | +Subject: $jsub | |
192 | +Content-Type: text/plain; charset=ISO-2022-JP | |
193 | +Content-Transfer-Encoding: 7bit | |
194 | +X-Simrm-Caller: PukiWiki | |
195 | + | |
196 | +$jbody | |
197 | +M_END; | |
198 | + | |
199 | + // simrm.rb の実行 | |
200 | + $ebody = exec_simrm($mail_str, $post['oldurl']); | |
201 | + if (!is_null($ebody)) { | |
202 | + $msg = 'Error of SimRM'; | |
203 | + $body .= $ebody; | |
204 | + return (array('msg' => $msg, 'body' => $body)); | |
205 | + } | |
206 | + | |
207 | + // 以下、正常終了時 | |
208 | + $msg = 'Result of SimRM'; | |
209 | + $body .= <<<EOS | |
210 | +<p>次の内容のメールを SimRM に渡しました。エラーがあれば、宛先にメールが送られます。 | |
211 | +<table border="1"> | |
212 | + <tr><td>宛先</td><td>{$post['to']}</td></tr> | |
213 | + <tr><td>件名</td><td>{$post['subject']}</td></tr> | |
214 | + <tr><td>本文</td><td>{$post['body']}</td></tr> | |
215 | +</table> | |
216 | +</p> | |
217 | +<a href="{$post['oldurl']}">前のページに戻る</a> | |
218 | +EOS; | |
219 | + return (array('msg' => $msg, 'body' => $body)); | |
220 | +} | |
221 | + | |
222 | + | |
223 | +/** | |
224 | + * プラグインのメイン関数。 | |
225 | + * 通常の GET 時に表示される。 | |
226 | + */ | |
227 | +function plugin_simrm_convert() | |
228 | +{ | |
229 | + global $script, $vars; | |
230 | + $_page = isset($vars['page']) ? $vars['page'] : ''; | |
231 | + $r_page = rawurlencode($_page); | |
232 | + | |
233 | + // 送信フォームの作成 | |
234 | + $ret = <<<EOS | |
235 | +<h3>SimRM plugin</h3> | |
236 | +<p> | |
237 | +複数アドレスを記述する際は ', ' (カンマと半角スペース) で区切ってください。<br/> | |
238 | +(例: foo@abc.com, hoo@a.b.c.com, haa@a.b.c.net)<br/> | |
239 | +また、件名に help と書くことでヘルプメールが宛先に送られます。 | |
240 | +</p> | |
241 | +<p> | |
242 | +<form action="$script?plugin=simrm" method="post"> | |
243 | + 宛先: <input type="text" name="to" value="" size="50" /><br/> | |
244 | + 件名: <input type="text" name="subject" value="" size="50" /><br/> | |
245 | + 内容: <textarea name="body" cols="60" rows="10"></textarea><br/> | |
246 | + <input type="hidden" name="plugin" value="simrm" /> | |
247 | + <input type="hidden" name="mode" value="submit" /> | |
248 | + <input type="hidden" name="oldurl" value="$script?$r_page" /> | |
249 | + <input type="submit" value="送信"/> | |
250 | + <input type="reset" value="クリア"/> | |
251 | +</form> | |
252 | +</p> | |
253 | +EOS; | |
254 | + // 既存ファイルのリスト作成 | |
255 | + $ret .= make_reserved_str(); | |
256 | + return $ret; | |
257 | +} | |
258 | + | |
259 | +?> |
@@ -0,0 +1,837 @@ | ||
1 | +<?php | |
2 | +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ | |
3 | +// +-----------------------------------------------------------------------+ | |
4 | +// | Copyright (c) 2002-2003 Richard Heyes | | |
5 | +// | Copyright (c) 2003-2005 The PHP Group | | |
6 | +// | All rights reserved. | | |
7 | +// | | | |
8 | +// | Redistribution and use in source and binary forms, with or without | | |
9 | +// | modification, are permitted provided that the following conditions | | |
10 | +// | are met: | | |
11 | +// | | | |
12 | +// | o Redistributions of source code must retain the above copyright | | |
13 | +// | notice, this list of conditions and the following disclaimer. | | |
14 | +// | o Redistributions in binary form must reproduce the above copyright | | |
15 | +// | notice, this list of conditions and the following disclaimer in the | | |
16 | +// | documentation and/or other materials provided with the distribution.| | |
17 | +// | o The names of the authors may not be used to endorse or promote | | |
18 | +// | products derived from this software without specific prior written | | |
19 | +// | permission. | | |
20 | +// | | | |
21 | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | | |
22 | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | | |
23 | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | | |
24 | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | | |
25 | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | | |
26 | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | | |
27 | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | | |
28 | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | | |
29 | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | | |
30 | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | | |
31 | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | | |
32 | +// | | | |
33 | +// +-----------------------------------------------------------------------+ | |
34 | +// | Author: Richard Heyes <richard@phpguru.org> | | |
35 | +// +-----------------------------------------------------------------------+ | |
36 | + | |
37 | +require_once 'PEAR.php'; | |
38 | + | |
39 | +/** | |
40 | +* +----------------------------- IMPORTANT ------------------------------+ | |
41 | +* | Usage of this class compared to native php extensions such as | | |
42 | +* | mailparse or imap, is slow and may be feature deficient. If available| | |
43 | +* | you are STRONGLY recommended to use the php extensions. | | |
44 | +* +----------------------------------------------------------------------+ | |
45 | +* | |
46 | +* Mime Decoding class | |
47 | +* | |
48 | +* This class will parse a raw mime email and return | |
49 | +* the structure. Returned structure is similar to | |
50 | +* that returned by imap_fetchstructure(). | |
51 | +* | |
52 | +* USAGE: (assume $input is your raw email) | |
53 | +* | |
54 | +* $decode = new Mail_mimeDecode($input, "\r\n"); | |
55 | +* $structure = $decode->decode(); | |
56 | +* print_r($structure); | |
57 | +* | |
58 | +* Or statically: | |
59 | +* | |
60 | +* $params['input'] = $input; | |
61 | +* $structure = Mail_mimeDecode::decode($params); | |
62 | +* print_r($structure); | |
63 | +* | |
64 | +* TODO: | |
65 | +* o Implement multipart/appledouble | |
66 | +* o UTF8: ??? | |
67 | + | |
68 | + > 4. We have also found a solution for decoding the UTF-8 | |
69 | + > headers. Therefore I made the following function: | |
70 | + > | |
71 | + > function decode_utf8($txt) { | |
72 | + > $trans=array("ナ‘"=>"テオ","ナア"=>"テサ","ナ?=>"テ•","ナー" | |
73 | + =>"テ›"); | |
74 | + > $txt=strtr($txt,$trans); | |
75 | + > return(utf8_decode($txt)); | |
76 | + > } | |
77 | + > | |
78 | + > And I have inserted the following line to the class: | |
79 | + > | |
80 | + > if (strtolower($charset)=="utf-8") $text=decode_utf8($text); | |
81 | + > | |
82 | + > ... before the following one in the "_decodeHeader" function: | |
83 | + > | |
84 | + > $input = str_replace($encoded, $text, $input); | |
85 | + > | |
86 | + > This way from now on it can easily decode the UTF-8 headers too. | |
87 | + | |
88 | +* | |
89 | +* @author Richard Heyes <richard@phpguru.org> | |
90 | +* @version $Revision: 1.46 $ | |
91 | +* @package Mail | |
92 | +*/ | |
93 | +class Mail_mimeDecode extends PEAR | |
94 | +{ | |
95 | + /** | |
96 | + * The raw email to decode | |
97 | + * @var string | |
98 | + */ | |
99 | + var $_input; | |
100 | + | |
101 | + /** | |
102 | + * The header part of the input | |
103 | + * @var string | |
104 | + */ | |
105 | + var $_header; | |
106 | + | |
107 | + /** | |
108 | + * The body part of the input | |
109 | + * @var string | |
110 | + */ | |
111 | + var $_body; | |
112 | + | |
113 | + /** | |
114 | + * If an error occurs, this is used to store the message | |
115 | + * @var string | |
116 | + */ | |
117 | + var $_error; | |
118 | + | |
119 | + /** | |
120 | + * Flag to determine whether to include bodies in the | |
121 | + * returned object. | |
122 | + * @var boolean | |
123 | + */ | |
124 | + var $_include_bodies; | |
125 | + | |
126 | + /** | |
127 | + * Flag to determine whether to decode bodies | |
128 | + * @var boolean | |
129 | + */ | |
130 | + var $_decode_bodies; | |
131 | + | |
132 | + /** | |
133 | + * Flag to determine whether to decode headers | |
134 | + * @var boolean | |
135 | + */ | |
136 | + var $_decode_headers; | |
137 | + | |
138 | + /** | |
139 | + * Constructor. | |
140 | + * | |
141 | + * Sets up the object, initialise the variables, and splits and | |
142 | + * stores the header and body of the input. | |
143 | + * | |
144 | + * @param string The input to decode | |
145 | + * @access public | |
146 | + */ | |
147 | + function Mail_mimeDecode($input) | |
148 | + { | |
149 | + list($header, $body) = $this->_splitBodyHeader($input); | |
150 | + | |
151 | + $this->_input = $input; | |
152 | + $this->_header = $header; | |
153 | + $this->_body = $body; | |
154 | + $this->_decode_bodies = false; | |
155 | + $this->_include_bodies = true; | |
156 | + } | |
157 | + | |
158 | + /** | |
159 | + * Begins the decoding process. If called statically | |
160 | + * it will create an object and call the decode() method | |
161 | + * of it. | |
162 | + * | |
163 | + * @param array An array of various parameters that determine | |
164 | + * various things: | |
165 | + * include_bodies - Whether to include the body in the returned | |
166 | + * object. | |
167 | + * decode_bodies - Whether to decode the bodies | |
168 | + * of the parts. (Transfer encoding) | |
169 | + * decode_headers - Whether to decode headers | |
170 | + * input - If called statically, this will be treated | |
171 | + * as the input | |
172 | + * @return object Decoded results | |
173 | + * @access public | |
174 | + */ | |
175 | + function decode($params = null) | |
176 | + { | |
177 | + // determine if this method has been called statically | |
178 | + $isStatic = !(isset($this) && get_class($this) == __CLASS__); | |
179 | + | |
180 | + // Have we been called statically? | |
181 | + // If so, create an object and pass details to that. | |
182 | + if ($isStatic AND isset($params['input'])) { | |
183 | + | |
184 | + $obj = new Mail_mimeDecode($params['input']); | |
185 | + $structure = $obj->decode($params); | |
186 | + | |
187 | + // Called statically but no input | |
188 | + } elseif ($isStatic) { | |
189 | + return PEAR::raiseError('Called statically and no input given'); | |
190 | + | |
191 | + // Called via an object | |
192 | + } else { | |
193 | + $this->_include_bodies = isset($params['include_bodies']) ? | |
194 | + $params['include_bodies'] : false; | |
195 | + $this->_decode_bodies = isset($params['decode_bodies']) ? | |
196 | + $params['decode_bodies'] : false; | |
197 | + $this->_decode_headers = isset($params['decode_headers']) ? | |
198 | + $params['decode_headers'] : false; | |
199 | + | |
200 | + $structure = $this->_decode($this->_header, $this->_body); | |
201 | + if ($structure === false) { | |
202 | + $structure = $this->raiseError($this->_error); | |
203 | + } | |
204 | + } | |
205 | + | |
206 | + return $structure; | |
207 | + } | |
208 | + | |
209 | + /** | |
210 | + * Performs the decoding. Decodes the body string passed to it | |
211 | + * If it finds certain content-types it will call itself in a | |
212 | + * recursive fashion | |
213 | + * | |
214 | + * @param string Header section | |
215 | + * @param string Body section | |
216 | + * @return object Results of decoding process | |
217 | + * @access private | |
218 | + */ | |
219 | + function _decode($headers, $body, $default_ctype = 'text/plain') | |
220 | + { | |
221 | + $return = new stdClass; | |
222 | + $return->headers = array(); | |
223 | + $headers = $this->_parseHeaders($headers); | |
224 | + | |
225 | + foreach ($headers as $value) { | |
226 | + if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) { | |
227 | + $return->headers[strtolower($value['name'])] = array($return->headers[strtolower($value['name'])]); | |
228 | + $return->headers[strtolower($value['name'])][] = $value['value']; | |
229 | + | |
230 | + } elseif (isset($return->headers[strtolower($value['name'])])) { | |
231 | + $return->headers[strtolower($value['name'])][] = $value['value']; | |
232 | + | |
233 | + } else { | |
234 | + $return->headers[strtolower($value['name'])] = $value['value']; | |
235 | + } | |
236 | + } | |
237 | + | |
238 | + reset($headers); | |
239 | + while (list($key, $value) = each($headers)) { | |
240 | + $headers[$key]['name'] = strtolower($headers[$key]['name']); | |
241 | + switch ($headers[$key]['name']) { | |
242 | + | |
243 | + case 'content-type': | |
244 | + $content_type = $this->_parseHeaderValue($headers[$key]['value']); | |
245 | + | |
246 | + if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) { | |
247 | + $return->ctype_primary = $regs[1]; | |
248 | + $return->ctype_secondary = $regs[2]; | |
249 | + } | |
250 | + | |
251 | + if (isset($content_type['other'])) { | |
252 | + while (list($p_name, $p_value) = each($content_type['other'])) { | |
253 | + $return->ctype_parameters[$p_name] = $p_value; | |
254 | + } | |
255 | + } | |
256 | + break; | |
257 | + | |
258 | + case 'content-disposition': | |
259 | + $content_disposition = $this->_parseHeaderValue($headers[$key]['value']); | |
260 | + $return->disposition = $content_disposition['value']; | |
261 | + if (isset($content_disposition['other'])) { | |
262 | + while (list($p_name, $p_value) = each($content_disposition['other'])) { | |
263 | + $return->d_parameters[$p_name] = $p_value; | |
264 | + } | |
265 | + } | |
266 | + break; | |
267 | + | |
268 | + case 'content-transfer-encoding': | |
269 | + $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']); | |
270 | + break; | |
271 | + } | |
272 | + } | |
273 | + | |
274 | + if (isset($content_type)) { | |
275 | + switch (strtolower($content_type['value'])) { | |
276 | + case 'text/plain': | |
277 | + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; | |
278 | + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; | |
279 | + break; | |
280 | + | |
281 | + case 'text/html': | |
282 | + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; | |
283 | + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; | |
284 | + break; | |
285 | + | |
286 | + case 'multipart/parallel': | |
287 | + case 'multipart/report': // RFC1892 | |
288 | + case 'multipart/signed': // PGP | |
289 | + case 'multipart/digest': | |
290 | + case 'multipart/alternative': | |
291 | + case 'multipart/related': | |
292 | + case 'multipart/mixed': | |
293 | + if(!isset($content_type['other']['boundary'])){ | |
294 | + $this->_error = 'No boundary found for ' . $content_type['value'] . ' part'; | |
295 | + return false; | |
296 | + } | |
297 | + | |
298 | + $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain'; | |
299 | + | |
300 | + $parts = $this->_boundarySplit($body, $content_type['other']['boundary']); | |
301 | + for ($i = 0; $i < count($parts); $i++) { | |
302 | + list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]); | |
303 | + $part = $this->_decode($part_header, $part_body, $default_ctype); | |
304 | + if($part === false) | |
305 | + $part = $this->raiseError($this->_error); | |
306 | + $return->parts[] = $part; | |
307 | + } | |
308 | + break; | |
309 | + | |
310 | + case 'message/rfc822': | |
311 | + $obj = &new Mail_mimeDecode($body); | |
312 | + $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies, | |
313 | + 'decode_bodies' => $this->_decode_bodies, | |
314 | + 'decode_headers' => $this->_decode_headers)); | |
315 | + unset($obj); | |
316 | + break; | |
317 | + | |
318 | + default: | |
319 | + if(!isset($content_transfer_encoding['value'])) | |
320 | + $content_transfer_encoding['value'] = '7bit'; | |
321 | + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null; | |
322 | + break; | |
323 | + } | |
324 | + | |
325 | + } else { | |
326 | + $ctype = explode('/', $default_ctype); | |
327 | + $return->ctype_primary = $ctype[0]; | |
328 | + $return->ctype_secondary = $ctype[1]; | |
329 | + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null; | |
330 | + } | |
331 | + | |
332 | + return $return; | |
333 | + } | |
334 | + | |
335 | + /** | |
336 | + * Given the output of the above function, this will return an | |
337 | + * array of references to the parts, indexed by mime number. | |
338 | + * | |
339 | + * @param object $structure The structure to go through | |
340 | + * @param string $mime_number Internal use only. | |
341 | + * @return array Mime numbers | |
342 | + */ | |
343 | + function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '') | |
344 | + { | |
345 | + $return = array(); | |
346 | + if (!empty($structure->parts)) { | |
347 | + if ($mime_number != '') { | |
348 | + $structure->mime_id = $prepend . $mime_number; | |
349 | + $return[$prepend . $mime_number] = &$structure; | |
350 | + } | |
351 | + for ($i = 0; $i < count($structure->parts); $i++) { | |
352 | + | |
353 | + | |
354 | + if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') { | |
355 | + $prepend = $prepend . $mime_number . '.'; | |
356 | + $_mime_number = ''; | |
357 | + } else { | |
358 | + $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1)); | |
359 | + } | |
360 | + | |
361 | + $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend); | |
362 | + foreach ($arr as $key => $val) { | |
363 | + $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key]; | |
364 | + } | |
365 | + } | |
366 | + } else { | |
367 | + if ($mime_number == '') { | |
368 | + $mime_number = '1'; | |
369 | + } | |
370 | + $structure->mime_id = $prepend . $mime_number; | |
371 | + $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; | |
372 | + } | |
373 | + | |
374 | + return $return; | |
375 | + } | |
376 | + | |
377 | + /** | |
378 | + * Given a string containing a header and body | |
379 | + * section, this function will split them (at the first | |
380 | + * blank line) and return them. | |
381 | + * | |
382 | + * @param string Input to split apart | |
383 | + * @return array Contains header and body section | |
384 | + * @access private | |
385 | + */ | |
386 | + function _splitBodyHeader($input) | |
387 | + { | |
388 | + if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) { | |
389 | + return array($match[1], $match[2]); | |
390 | + } | |
391 | + $this->_error = 'Could not split header and body'; | |
392 | + return false; | |
393 | + } | |
394 | + | |
395 | + /** | |
396 | + * Parse headers given in $input and return | |
397 | + * as assoc array. | |
398 | + * | |
399 | + * @param string Headers to parse | |
400 | + * @return array Contains parsed headers | |
401 | + * @access private | |
402 | + */ | |
403 | + function _parseHeaders($input) | |
404 | + { | |
405 | + | |
406 | + if ($input !== '') { | |
407 | + // Unfold the input | |
408 | + $input = preg_replace("/\r?\n/", "\r\n", $input); | |
409 | + $input = preg_replace("/\r\n(\t| )+/", ' ', $input); | |
410 | + $headers = explode("\r\n", trim($input)); | |
411 | + | |
412 | + foreach ($headers as $value) { | |
413 | + $hdr_name = substr($value, 0, $pos = strpos($value, ':')); | |
414 | + $hdr_value = substr($value, $pos+1); | |
415 | + if($hdr_value[0] == ' ') | |
416 | + $hdr_value = substr($hdr_value, 1); | |
417 | + | |
418 | + $return[] = array( | |
419 | + 'name' => $hdr_name, | |
420 | + 'value' => $this->_decode_headers ? $this->_decodeHeader($hdr_value) : $hdr_value | |
421 | + ); | |
422 | + } | |
423 | + } else { | |
424 | + $return = array(); | |
425 | + } | |
426 | + | |
427 | + return $return; | |
428 | + } | |
429 | + | |
430 | + /** | |
431 | + * Function to parse a header value, | |
432 | + * extract first part, and any secondary | |
433 | + * parts (after ;) This function is not as | |
434 | + * robust as it could be. Eg. header comments | |
435 | + * in the wrong place will probably break it. | |
436 | + * | |
437 | + * @param string Header value to parse | |
438 | + * @return array Contains parsed result | |
439 | + * @access private | |
440 | + */ | |
441 | + function _parseHeaderValue($input) | |
442 | + { | |
443 | + | |
444 | + if (($pos = strpos($input, ';')) !== false) { | |
445 | + | |
446 | + $return['value'] = trim(substr($input, 0, $pos)); | |
447 | + $input = trim(substr($input, $pos+1)); | |
448 | + | |
449 | + if (strlen($input) > 0) { | |
450 | + | |
451 | + // This splits on a semi-colon, if there's no preceeding backslash | |
452 | + // Now works with quoted values; had to glue the \; breaks in PHP | |
453 | + // the regex is already bordering on incomprehensible | |
454 | + $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/'; | |
455 | + preg_match_all($splitRegex, $input, $matches); | |
456 | + $parameters = array(); | |
457 | + for ($i=0; $i<count($matches[0]); $i++) { | |
458 | + $param = $matches[0][$i]; | |
459 | + while (substr($param, -2) == '\;') { | |
460 | + $param .= $matches[0][++$i]; | |
461 | + } | |
462 | + $parameters[] = $param; | |
463 | + } | |
464 | + | |
465 | + for ($i = 0; $i < count($parameters); $i++) { | |
466 | + $param_name = trim(substr($parameters[$i], 0, $pos = strpos($parameters[$i], '=')), "'\";\t\\ "); | |
467 | + $param_value = trim(str_replace('\;', ';', substr($parameters[$i], $pos + 1)), "'\";\t\\ "); | |
468 | + if ($param_value[0] == '"') { | |
469 | + $param_value = substr($param_value, 1, -1); | |
470 | + } | |
471 | + $return['other'][$param_name] = $param_value; | |
472 | + $return['other'][strtolower($param_name)] = $param_value; | |
473 | + } | |
474 | + } | |
475 | + } else { | |
476 | + $return['value'] = trim($input); | |
477 | + } | |
478 | + | |
479 | + return $return; | |
480 | + } | |
481 | + | |
482 | + /** | |
483 | + * This function splits the input based | |
484 | + * on the given boundary | |
485 | + * | |
486 | + * @param string Input to parse | |
487 | + * @return array Contains array of resulting mime parts | |
488 | + * @access private | |
489 | + */ | |
490 | + function _boundarySplit($input, $boundary) | |
491 | + { | |
492 | + $parts = array(); | |
493 | + | |
494 | + $bs_possible = substr($boundary, 2, -2); | |
495 | + $bs_check = '\"' . $bs_possible . '\"'; | |
496 | + | |
497 | + if ($boundary == $bs_check) { | |
498 | + $boundary = $bs_possible; | |
499 | + } | |
500 | + | |
501 | + $tmp = explode('--' . $boundary, $input); | |
502 | + | |
503 | + for ($i = 1; $i < count($tmp) - 1; $i++) { | |
504 | + $parts[] = $tmp[$i]; | |
505 | + } | |
506 | + | |
507 | + return $parts; | |
508 | + } | |
509 | + | |
510 | + /** | |
511 | + * Given a header, this function will decode it | |
512 | + * according to RFC2047. Probably not *exactly* | |
513 | + * conformant, but it does pass all the given | |
514 | + * examples (in RFC2047). | |
515 | + * | |
516 | + * @param string Input header value to decode | |
517 | + * @return string Decoded header value | |
518 | + * @access private | |
519 | + */ | |
520 | + function _decodeHeader($input) | |
521 | + { | |
522 | + // Remove white space between encoded-words | |
523 | + $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); | |
524 | + | |
525 | + // For each encoded-word... | |
526 | + while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { | |
527 | + | |
528 | + $encoded = $matches[1]; | |
529 | + $charset = $matches[2]; | |
530 | + $encoding = $matches[3]; | |
531 | + $text = $matches[4]; | |
532 | + | |
533 | + switch (strtolower($encoding)) { | |
534 | + case 'b': | |
535 | + $text = base64_decode($text); | |
536 | + break; | |
537 | + | |
538 | + case 'q': | |
539 | + $text = str_replace('_', ' ', $text); | |
540 | + preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); | |
541 | + foreach($matches[1] as $value) | |
542 | + $text = str_replace('='.$value, chr(hexdec($value)), $text); | |
543 | + break; | |
544 | + } | |
545 | + | |
546 | + $input = str_replace($encoded, $text, $input); | |
547 | + } | |
548 | + | |
549 | + return $input; | |
550 | + } | |
551 | + | |
552 | + /** | |
553 | + * Given a body string and an encoding type, | |
554 | + * this function will decode and return it. | |
555 | + * | |
556 | + * @param string Input body to decode | |
557 | + * @param string Encoding type to use. | |
558 | + * @return string Decoded body | |
559 | + * @access private | |
560 | + */ | |
561 | + function _decodeBody($input, $encoding = '7bit') | |
562 | + { | |
563 | + switch (strtolower($encoding)) { | |
564 | + case '7bit': | |
565 | + return $input; | |
566 | + break; | |
567 | + | |
568 | + case 'quoted-printable': | |
569 | + return $this->_quotedPrintableDecode($input); | |
570 | + break; | |
571 | + | |
572 | + case 'base64': | |
573 | + return base64_decode($input); | |
574 | + break; | |
575 | + | |
576 | + default: | |
577 | + return $input; | |
578 | + } | |
579 | + } | |
580 | + | |
581 | + /** | |
582 | + * Given a quoted-printable string, this | |
583 | + * function will decode and return it. | |
584 | + * | |
585 | + * @param string Input body to decode | |
586 | + * @return string Decoded body | |
587 | + * @access private | |
588 | + */ | |
589 | + function _quotedPrintableDecode($input) | |
590 | + { | |
591 | + // Remove soft line breaks | |
592 | + $input = preg_replace("/=\r?\n/", '', $input); | |
593 | + | |
594 | + // Replace encoded characters | |
595 | + $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input); | |
596 | + | |
597 | + return $input; | |
598 | + } | |
599 | + | |
600 | + /** | |
601 | + * Checks the input for uuencoded files and returns | |
602 | + * an array of them. Can be called statically, eg: | |
603 | + * | |
604 | + * $files =& Mail_mimeDecode::uudecode($some_text); | |
605 | + * | |
606 | + * It will check for the begin 666 ... end syntax | |
607 | + * however and won't just blindly decode whatever you | |
608 | + * pass it. | |
609 | + * | |
610 | + * @param string Input body to look for attahcments in | |
611 | + * @return array Decoded bodies, filenames and permissions | |
612 | + * @access public | |
613 | + * @author Unknown | |
614 | + */ | |
615 | + function &uudecode($input) | |
616 | + { | |
617 | + // Find all uuencoded sections | |
618 | + preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches); | |
619 | + | |
620 | + for ($j = 0; $j < count($matches[3]); $j++) { | |
621 | + | |
622 | + $str = $matches[3][$j]; | |
623 | + $filename = $matches[2][$j]; | |
624 | + $fileperm = $matches[1][$j]; | |
625 | + | |
626 | + $file = ''; | |
627 | + $str = preg_split("/\r?\n/", trim($str)); | |
628 | + $strlen = count($str); | |
629 | + | |
630 | + for ($i = 0; $i < $strlen; $i++) { | |
631 | + $pos = 1; | |
632 | + $d = 0; | |
633 | + $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077); | |
634 | + | |
635 | + while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) { | |
636 | + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); | |
637 | + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); | |
638 | + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); | |
639 | + $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20); | |
640 | + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); | |
641 | + | |
642 | + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); | |
643 | + | |
644 | + $file .= chr(((($c2 - ' ') & 077) << 6) | (($c3 - ' ') & 077)); | |
645 | + | |
646 | + $pos += 4; | |
647 | + $d += 3; | |
648 | + } | |
649 | + | |
650 | + if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) { | |
651 | + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); | |
652 | + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); | |
653 | + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); | |
654 | + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); | |
655 | + | |
656 | + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); | |
657 | + | |
658 | + $pos += 3; | |
659 | + $d += 2; | |
660 | + } | |
661 | + | |
662 | + if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) { | |
663 | + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); | |
664 | + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); | |
665 | + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); | |
666 | + | |
667 | + } | |
668 | + } | |
669 | + $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file); | |
670 | + } | |
671 | + | |
672 | + return $files; | |
673 | + } | |
674 | + | |
675 | + /** | |
676 | + * getSendArray() returns the arguments required for Mail::send() | |
677 | + * used to build the arguments for a mail::send() call | |
678 | + * | |
679 | + * Usage: | |
680 | + * $mailtext = Full email (for example generated by a template) | |
681 | + * $decoder = new Mail_mimeDecode($mailtext); | |
682 | + * $parts = $decoder->getSendArray(); | |
683 | + * if (!PEAR::isError($parts) { | |
684 | + * list($recipents,$headers,$body) = $parts; | |
685 | + * $mail = Mail::factory('smtp'); | |
686 | + * $mail->send($recipents,$headers,$body); | |
687 | + * } else { | |
688 | + * echo $parts->message; | |
689 | + * } | |
690 | + * @return mixed array of recipeint, headers,body or Pear_Error | |
691 | + * @access public | |
692 | + * @author Alan Knowles <alan@akbkhome.com> | |
693 | + */ | |
694 | + function getSendArray() | |
695 | + { | |
696 | + // prevent warning if this is not set | |
697 | + $this->_decode_headers = FALSE; | |
698 | + $headerlist =$this->_parseHeaders($this->_header); | |
699 | + $to = ""; | |
700 | + if (!$headerlist) { | |
701 | + return $this->raiseError("Message did not contain headers"); | |
702 | + } | |
703 | + foreach($headerlist as $item) { | |
704 | + $header[$item['name']] = $item['value']; | |
705 | + switch (strtolower($item['name'])) { | |
706 | + case "to": | |
707 | + case "cc": | |
708 | + case "bcc": | |
709 | + $to = ",".$item['value']; | |
710 | + default: | |
711 | + break; | |
712 | + } | |
713 | + } | |
714 | + if ($to == "") { | |
715 | + return $this->raiseError("Message did not contain any recipents"); | |
716 | + } | |
717 | + $to = substr($to,1); | |
718 | + return array($to,$header,$this->_body); | |
719 | + } | |
720 | + | |
721 | + /** | |
722 | + * Returns a xml copy of the output of | |
723 | + * Mail_mimeDecode::decode. Pass the output in as the | |
724 | + * argument. This function can be called statically. Eg: | |
725 | + * | |
726 | + * $output = $obj->decode(); | |
727 | + * $xml = Mail_mimeDecode::getXML($output); | |
728 | + * | |
729 | + * The DTD used for this should have been in the package. Or | |
730 | + * alternatively you can get it from cvs, or here: | |
731 | + * http://www.phpguru.org/xmail/xmail.dtd. | |
732 | + * | |
733 | + * @param object Input to convert to xml. This should be the | |
734 | + * output of the Mail_mimeDecode::decode function | |
735 | + * @return string XML version of input | |
736 | + * @access public | |
737 | + */ | |
738 | + function getXML($input) | |
739 | + { | |
740 | + $crlf = "\r\n"; | |
741 | + $output = '<?xml version=\'1.0\'?>' . $crlf . | |
742 | + '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf . | |
743 | + '<email>' . $crlf . | |
744 | + Mail_mimeDecode::_getXML($input) . | |
745 | + '</email>'; | |
746 | + | |
747 | + return $output; | |
748 | + } | |
749 | + | |
750 | + /** | |
751 | + * Function that does the actual conversion to xml. Does a single | |
752 | + * mimepart at a time. | |
753 | + * | |
754 | + * @param object Input to convert to xml. This is a mimepart object. | |
755 | + * It may or may not contain subparts. | |
756 | + * @param integer Number of tabs to indent | |
757 | + * @return string XML version of input | |
758 | + * @access private | |
759 | + */ | |
760 | + function _getXML($input, $indent = 1) | |
761 | + { | |
762 | + $htab = "\t"; | |
763 | + $crlf = "\r\n"; | |
764 | + $output = ''; | |
765 | + $headers = @(array)$input->headers; | |
766 | + | |
767 | + foreach ($headers as $hdr_name => $hdr_value) { | |
768 | + | |
769 | + // Multiple headers with this name | |
770 | + if (is_array($headers[$hdr_name])) { | |
771 | + for ($i = 0; $i < count($hdr_value); $i++) { | |
772 | + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent); | |
773 | + } | |
774 | + | |
775 | + // Only one header of this sort | |
776 | + } else { | |
777 | + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent); | |
778 | + } | |
779 | + } | |
780 | + | |
781 | + if (!empty($input->parts)) { | |
782 | + for ($i = 0; $i < count($input->parts); $i++) { | |
783 | + $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf . | |
784 | + Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) . | |
785 | + str_repeat($htab, $indent) . '</mimepart>' . $crlf; | |
786 | + } | |
787 | + } elseif (isset($input->body)) { | |
788 | + $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' . | |
789 | + $input->body . ']]></body>' . $crlf; | |
790 | + } | |
791 | + | |
792 | + return $output; | |
793 | + } | |
794 | + | |
795 | + /** | |
796 | + * Helper function to _getXML(). Returns xml of a header. | |
797 | + * | |
798 | + * @param string Name of header | |
799 | + * @param string Value of header | |
800 | + * @param integer Number of tabs to indent | |
801 | + * @return string XML version of input | |
802 | + * @access private | |
803 | + */ | |
804 | + function _getXML_helper($hdr_name, $hdr_value, $indent) | |
805 | + { | |
806 | + $htab = "\t"; | |
807 | + $crlf = "\r\n"; | |
808 | + $return = ''; | |
809 | + | |
810 | + $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value); | |
811 | + $new_hdr_name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name))); | |
812 | + | |
813 | + // Sort out any parameters | |
814 | + if (!empty($new_hdr_value['other'])) { | |
815 | + foreach ($new_hdr_value['other'] as $paramname => $paramvalue) { | |
816 | + $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf . | |
817 | + str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf . | |
818 | + str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf . | |
819 | + str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf; | |
820 | + } | |
821 | + | |
822 | + $params = implode('', $params); | |
823 | + } else { | |
824 | + $params = ''; | |
825 | + } | |
826 | + | |
827 | + $return = str_repeat($htab, $indent) . '<header>' . $crlf . | |
828 | + str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf . | |
829 | + str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf . | |
830 | + $params . | |
831 | + str_repeat($htab, $indent) . '</header>' . $crlf; | |
832 | + | |
833 | + return $return; | |
834 | + } | |
835 | + | |
836 | +} // End of class | |
837 | +?> |
@@ -0,0 +1,115 @@ | ||
1 | +# | |
2 | +# プラグインの基本クラス | |
3 | +# | |
4 | +# NOTE: このクラスの変更は、他のプラグインに影響を与える為、十分に注意すること | |
5 | +# | |
6 | +# $Id$ | |
7 | +# | |
8 | + | |
9 | +require 'lib/mailext.rb' | |
10 | + | |
11 | +$KCODE = 'e' | |
12 | + | |
13 | +# NOTE: SimRM 側の挙動について | |
14 | +# 1. 件名に日時情報付きのメールが届いたとき | |
15 | +# new で initialize() されたあとに mail_error が参照される。 | |
16 | +# mail_error が Plugin::NO_ERROR だった場合はメールが保存される。 | |
17 | +# mail_error が Plugin::ERROR だった場合はエラーの内容を得る為に get_mail_body() が実行される。 | |
18 | +# mail_error が Plugin::IGNORE_ME だった場合は速やかに処理を終える。 | |
19 | +# | |
20 | +# 2. 件名に日時情報が無いメールが届いたとき | |
21 | +# new で initialize() されたあとに mail_error が参照される。 | |
22 | +# mail_error が Plugin::IGNORE_ME 以外ならば送信メールの本文を得る為に get_mail_body() が実行される。 | |
23 | +# mail_error が Plugin::IGNORE_ME だった場合は速やかに処理を終える。 | |
24 | +# | |
25 | +# 3. 保存されていたメールが指定日時に送信されるとき | |
26 | +# new で initialize() されたあとに mail_error が参照される。 | |
27 | +# mail_error が Plugin::IGNORE_ME 以外ならば送信メールの本文を得る為に get_mail_body() が実行される。 | |
28 | +# mail_error が Plugin::IGNORE_ME だった場合は速やかに処理を終える。 | |
29 | +# (件名が every 系ならば、メールはそのまま残る) | |
30 | +# | |
31 | +# NOTE: | |
32 | +# initialize(), get_mail_body(), delete() は呼び側で例外のキャッチが行われる。 | |
33 | +# (しかし、プラグイン自身で例外の考慮をしておくのが望ましい) | |
34 | +# | |
35 | +# NOTE: | |
36 | +# サブクラスの再読込を可能とするため、サブクラスで定義する値は定数化しない | |
37 | +class Plugin | |
38 | + # 下記の定数群はプラグインごとに設定する | |
39 | + # NOTE: class 名がそれぞれ異なるのでコピペ時に注意 | |
40 | + # このクラスを使っていいか? | |
41 | + def Plugin.use_this_class? ; false ; end | |
42 | + # やや重いため、thread 処理を求めるか? | |
43 | + def Plugin.use_thread? ; false ; end | |
44 | + | |
45 | + # プラグインの目的 (一行で) | |
46 | + def Plugin.purpose ; '' ; end | |
47 | + # プラグインの著作者情報 (できれば連絡先も記載のこと) | |
48 | + def Plugin.author ; '' ; end | |
49 | + # プラグインのバージョン情報 | |
50 | + def Plugin.version ; '' ; end | |
51 | + | |
52 | + # @mail_error に設定する値 | |
53 | + NO_ERROR = 0 # 正常動作。結果のメールを送る | |
54 | + ERROR = 1 # エラーメールを送る | |
55 | + IGNORE_ME = 2 # エラーメールすら送らない | |
56 | + | |
57 | + # このメールにエラーがあるかどうか? | |
58 | + # NOTE: メール本文に間違いがある場合はすぐさまエラーメールを返すため、 | |
59 | + # この変数は initialize() 後に必ず参照される。 | |
60 | + @mail_error = NO_ERROR | |
61 | + attr_reader :mail_error | |
62 | + # エラーメールを送る際はエラー内容も記述しておくこと | |
63 | + @mail_error_msg = 'unknown' | |
64 | + attr_reader :mail_error_msg | |
65 | + | |
66 | + | |
67 | + # initialize(), get_mail_body() のとき、 | |
68 | + # どのような目的で利用されるのか知りたい (引数 mode) | |
69 | + MODE_SAVE = 1 | |
70 | + MODE_SEND = 2 | |
71 | + | |
72 | + # コンストラクタは Mail オブジェクトと引数文字列、モードを受け取る。 | |
73 | + def initialize(mail, opts, mode) | |
74 | + # メールの中をチェック | |
75 | + #your_check_function(mail, opts, mode) | |
76 | + # | |
77 | + # チェック関数の中でもいいが、必ず各種フラグも設定すること | |
78 | + #@mail_error = NO_ERROR | |
79 | + end | |
80 | + | |
81 | + | |
82 | + # initialize() か get_mail_body() のいずれかのタイミングで | |
83 | + # メールの本文を作成すること。 | |
84 | + def get_mail_body(mail, opts, mode) | |
85 | + return "not implemented" | |
86 | + end | |
87 | + | |
88 | + | |
89 | + # プラグインオブジェクトの廃棄時に必ず実行される。 | |
90 | + # オブジェクト生存時にファイル等を開きっぱなしにしている場合は、 | |
91 | + # この関数で後始末を行うこと。 | |
92 | + def delete() | |
93 | + # do something | |
94 | + end | |
95 | + | |
96 | + | |
97 | + # help 関数でプラグインの作者名とバージョンを表示しやすくする関数 | |
98 | + # NOTE: 引数はインスタンスではなくクラスであること | |
99 | + def Plugin.signature(c) | |
100 | + a = (c.author rescue '記載無し') | |
101 | + v = (c.version rescue '記載無し') | |
102 | + sig = " Author: #{a}\n" | |
103 | + sig += "Version: #{v}\n" | |
104 | + return sig | |
105 | + end | |
106 | + | |
107 | + | |
108 | + # メール件名 "plugin:help:PLUGIN_NAME" にて、 | |
109 | + # プラグインの使用方法を記述したメールを返信できるようにする。 | |
110 | + def Plugin.help | |
111 | + return '' | |
112 | + end | |
113 | +end | |
114 | + | |
115 | +__END__ |
@@ -0,0 +1,53 @@ | ||
1 | +# | |
2 | +# プラグインユーティリティモジュール | |
3 | +# | |
4 | +# $Id$ | |
5 | +# | |
6 | + | |
7 | +$KCODE = 'e' | |
8 | +require 'digest/md5' | |
9 | + | |
10 | +require "#{PLUGIN_DIR}/plugin.rb" | |
11 | + | |
12 | +module PUtils | |
13 | + # 以降の関数は全てモジュール関数として定義 | |
14 | + module_function | |
15 | + | |
16 | + # true -> OK | |
17 | + # 引数が nil のときも true を返す | |
18 | + def check_opts_for_cmd(opts) | |
19 | + return true if opts == nil | |
20 | + (opts =~ /[$<>|)(#&;]/) == nil | |
21 | + end | |
22 | + | |
23 | + | |
24 | + # 以下の定数は check_body_for_md5 でエラーメッセージを変更させるために利用 | |
25 | + MD5 = 1 | |
26 | + PASSWORD = 2 | |
27 | + | |
28 | + def check_body_for_md5(body, type) | |
29 | + case type | |
30 | + when PASSWORD | |
31 | + msg_nostr = 'パスワードが記述されていません' | |
32 | + msg_only = "空白文字 (\\s) のみで構成されたパスワードは受け付けません" | |
33 | + else | |
34 | + msg_nostr = "MD5 値を生成するために必要な文字列がありません" | |
35 | + msg_only = "空白文字 (\\s) のみで構成された文字列は受け付けません" | |
36 | + end | |
37 | + | |
38 | + str = body.to_s if body != nil | |
39 | + return Plugin::ERROR, msg_nostr if body == nil or str == "" | |
40 | + | |
41 | + str.sub!(/^\s+/, '') | |
42 | + str.sub!(/\s+$/, '') | |
43 | + return Plugin::ERROR, msg_only if str == "" | |
44 | + | |
45 | + return Plugin::NO_ERROR, '' | |
46 | + end | |
47 | + | |
48 | + | |
49 | + # NOTE: 先頭と末尾の空白文字列は削除する | |
50 | + def get_md5(str) | |
51 | + Digest::MD5.hexdigest(str.sub(/^\s+/, '').sub(/\s+$/, '')) | |
52 | + end | |
53 | +end |
@@ -0,0 +1,77 @@ | ||
1 | +# | |
2 | +# インストール方法: | |
3 | +# 1. コマンドで echo -n "YOUR_PASSWORD" | md5sum を実行し、 | |
4 | +# その MD5 値をこのファイルの PASSWORD に設定する | |
5 | +# 2. ls コマンドに渡したいオプションがあれば LS_OPTION に設定する | |
6 | +# 3. このファイルの use_this_class? を true にする | |
7 | +# | |
8 | +$KCODE = 'e' | |
9 | +require "#{PLUGIN_DIR}/plugin.rb" | |
10 | +require "#{PLUGIN_DIR}/plugin_utils.rb" | |
11 | + | |
12 | +class Plugin_ls < Plugin | |
13 | + # このプラグインを使う時に必要なパスワード値 | |
14 | + @@PASSWORD = '' # TODO: 適切なパスワードを用いて、MD5 値を記入すること | |
15 | + #@@PASSWORD = '25d55ad283aa400af464c76d713c07ad' # '12345678' の MD5 値 | |
16 | + #@@PASSWORD = 'ea703e7aa1efda0064eaa507d9e8ab7e' # 'hoge' の MD5 値 | |
17 | + | |
18 | + @@LS_OPTION = '-l' | |
19 | + | |
20 | + def Plugin_ls.use_this_class? ; false ; end | |
21 | + def Plugin_ls.use_thread? ; false ; end | |
22 | + | |
23 | + def Plugin_ls.purpose ; 'SimRM を実行しているマシン上で ls を行う' ; end | |
24 | + def Plugin_ls.author ; 'isr' ; end | |
25 | + def Plugin_ls.version ; '$Id$' ; end | |
26 | + | |
27 | + | |
28 | + def Plugin_ls.help | |
29 | + body = <<_EOS_ | |
30 | +SimRM を実行しているマシン上で ls コマンドを実行し、その結果を返します。 | |
31 | + | |
32 | +Usage: | |
33 | + plugin:ls:FILE_OR_DIRECTORY | |
34 | + | |
35 | + メールの件名に対象を記述し、メール本文にはパスワードを記述してください。 | |
36 | + 引数無しはエラーとなります。 | |
37 | + | |
38 | +制限: | |
39 | + - 複数ファイル、ディレクトリを引数に取ることは出来ません。 | |
40 | + | |
41 | +_EOS_ | |
42 | + return body | |
43 | + end | |
44 | + | |
45 | + | |
46 | + def initialize(mail, opts, mode) | |
47 | + # パスワードチェック | |
48 | + @mail_error, @mail_error_msg = PUtils::check_body_for_md5(mail.body, PUtils::PASSWORD) | |
49 | + return if @mail_error != NO_ERROR | |
50 | + | |
51 | + if PUtils::get_md5(mail.body.to_s) != @@PASSWORD | |
52 | + @mail_error = ERROR | |
53 | + @mail_error_msg = "パスワードが異なります" | |
54 | + return | |
55 | + end | |
56 | + | |
57 | + unless File.readable?(opts) | |
58 | + @mail_error = ERROR | |
59 | + @mail_error_msg = "'#{opts}' の読み込み権限がありません" | |
60 | + return | |
61 | + end | |
62 | + end | |
63 | + | |
64 | + | |
65 | + def get_mail_body(mail, opts, mode) | |
66 | + cmd = "ls #{@@LS_OPTION} #{opts}" | |
67 | + | |
68 | + body = "以下、`#{cmd}` の実行結果です\n" | |
69 | + body << "=================================================================\n" | |
70 | + body << `#{cmd} 2>&1` | |
71 | + body << "=================================================================\n" | |
72 | + body << "プロセスの戻り値は '#{$?.exitstatus}' でした\n" | |
73 | + return body | |
74 | + end | |
75 | +end | |
76 | + | |
77 | +__END__ |
@@ -0,0 +1,88 @@ | ||
1 | +# | |
2 | +# プラグインのサンプル | |
3 | +# プラグイン作成時、参考にして下さい | |
4 | +# | |
5 | +# $Id$ | |
6 | +# | |
7 | + | |
8 | +# おまじない | |
9 | +$KCODE = 'e' | |
10 | + | |
11 | +# おまじない | |
12 | +require "#{PLUGIN_DIR}/plugin.rb" | |
13 | + | |
14 | +# SimRM のもつ Utils クラスを一部で利用する | |
15 | +require "#{LIB_DIR}/utils.rb" | |
16 | + | |
17 | + | |
18 | +class Plugin_sample < Plugin | |
19 | + # 下記の定数群はプラグインごとに設定する | |
20 | + # このクラスを使っていいか? | |
21 | + def Plugin_sample.use_this_class? ; true ; end | |
22 | + # やや重いため、thread 処理を求めるか? | |
23 | + def Plugin_sample.use_thread? ; false ; end | |
24 | + | |
25 | + # プラグインの目的 (一行で) | |
26 | + def Plugin_sample.purpose ; 'プラグインのサンプルとして何かを実行する' ; end | |
27 | + # プラグインの著作者情報 (できれば連絡先も記載のこと) | |
28 | + def Plugin_sample.author ; 'isr' ; end | |
29 | + # プラグインのバージョン情報 | |
30 | + def Plugin_sample.version ; '$Id$' ; end | |
31 | + | |
32 | + | |
33 | + # メール件名 "plugin:help:PLUGIN_NAME" にて、 | |
34 | + # プラグインの使用方法を記述したメールを返信できるようにする。 | |
35 | + def Plugin_sample.help | |
36 | + body = "プラグインのサンプルです。\n" | |
37 | + body += "プラグインディレクトリ内のプラグイン利用可否情報 (use_this_class?) を返します。\n" | |
38 | + body += "引数などは取りません。(無視します)\n\n" | |
39 | + return body | |
40 | + end | |
41 | + | |
42 | + | |
43 | + def initialize(mail, opts, mode) | |
44 | + @mail_error = NO_ERROR | |
45 | + end | |
46 | + | |
47 | + | |
48 | + # サンプルとして、プラグインディレクトリ内のプラグイン利用可否を列挙する | |
49 | + def get_mail_body(mail, opts, mode) | |
50 | + body = "Hi! This is a sample plugin!\n" + | |
51 | + "You can create some nice plugins easily, maybe ;-)\n\n" | |
52 | + | |
53 | + # 既に「おまじない」で使っているが、 | |
54 | + # PLUGIN_DIR は SimRM 側で定義されている値 | |
55 | + # 基本的に SimRM が持つ外部変数、定数値にはアクセスできる | |
56 | + # NOTE: だからといって外部変数の値を更新しないこと | |
57 | + body += "Plugin Directory: #{PLUGIN_DIR}\n" | |
58 | + | |
59 | + # 例外の発生は常に考慮すること。 | |
60 | + # initialize, get_mail_body を呼び出す側は rescue 処置をしているが、 | |
61 | + # 戻り値が不明となってしまい適当な文字列を返すことになってしまう。 | |
62 | + # それはできる限り防ぐ。 | |
63 | + begin | |
64 | + # ディレクトリ内の smplg.rb ファイルを検索する | |
65 | + Dir.glob("#{PLUGIN_DIR}/*.smplg.rb").each {|f| | |
66 | + ret = Utils::grep(f, /use_this_class\?\s*;/) | |
67 | + if ret.size == 0 | |
68 | + body << "#{f}: No use_this_class?\n" | |
69 | + else | |
70 | + # #{s} には改行が含まれている | |
71 | + ret.each {|s| body << "#{f}: #{s}" } | |
72 | + end | |
73 | + } | |
74 | + rescue => e | |
75 | + body << "\n-----------------------------\n" | |
76 | + body << "Exception occurs!" | |
77 | + body << "\n-----------------------------\n" | |
78 | + body << "Message: #{e.message}\n" | |
79 | + body << "Backtrace:\n" | |
80 | + body << e.backtrace.join("\n") | |
81 | + body << "\n-----------------------------\n" | |
82 | + end | |
83 | + | |
84 | + return body | |
85 | + end | |
86 | +end | |
87 | + | |
88 | +__END__ |
@@ -0,0 +1,101 @@ | ||
1 | +# | |
2 | +# fortune プラグイン | |
3 | +# | |
4 | +# システム要件: | |
5 | +# - fortune プラグラムがインストールされていること | |
6 | +# | |
7 | +# インストール方法: | |
8 | +# 1. fortune コマンドのフルパスを FORTUNE に設定する | |
9 | +# (fortune コマンドがインストールされていなければインストールする) | |
10 | +# | |
11 | +# 2. fortune コマンドに渡したいオプションを | |
12 | +# FORTUNE_OPTS に設定する | |
13 | +# | |
14 | +# 3. このファイルの use_this_class? を true にする | |
15 | +# | |
16 | +# $Id$ | |
17 | +# | |
18 | + | |
19 | +$KCODE = 'e' | |
20 | +require "#{PLUGIN_DIR}/plugin.rb" | |
21 | +require "#{PLUGIN_DIR}/plugin_utils.rb" | |
22 | + | |
23 | + | |
24 | +# NOTE: 再読込できるように定数は定義しない | |
25 | +class Plugin_fortune < Plugin | |
26 | + # nil or '' ならば一般的なパスをチェックする | |
27 | + @@FORTUNE = nil | |
28 | + @@FORTUNE_OPTS = '' | |
29 | + | |
30 | + def Plugin_fortune.use_this_class? ; true ; end | |
31 | + def Plugin_fortune.use_thread? ; false ; end | |
32 | + | |
33 | + def Plugin_fortune.purpose ; '為になるような、ならないような言葉をランダムにセレクトしてお届け' ; end | |
34 | + def Plugin_fortune.author ; 'isr' ; end | |
35 | + def Plugin_fortune.version ; '$Id$' ; end | |
36 | + | |
37 | + def Plugin_fortune.help | |
38 | + body = <<_EOS_ | |
39 | +為になるような、ならないような言葉をランダムにセレクトしてお届けするプラグイン。 | |
40 | + | |
41 | +Usage: | |
42 | + plugin:fortune[:OPTION [OPTION...]] | |
43 | + | |
44 | +OPTION はこのシステムで使われている fortune プログラムに合わせてください。 | |
45 | + | |
46 | +_EOS_ | |
47 | + return body | |
48 | + end | |
49 | + | |
50 | + | |
51 | + def get_fortune | |
52 | + if @@FORTUNE != nil and @@FORTUNE != '' | |
53 | + return @@FORTUNE if File.executable?(@@FORTUNE) | |
54 | + end | |
55 | + | |
56 | + # 各 OS に存在する fortune パスをチェック | |
57 | + [ | |
58 | + '/sw/bin/fortune', | |
59 | + '/usr/bin/fortune', | |
60 | + '/usr/games/fortune' | |
61 | + ].each {|bin| | |
62 | + return bin if File.executable?(bin) | |
63 | + } | |
64 | + return nil | |
65 | + end | |
66 | + | |
67 | + def initialize(mail, opts, mode) | |
68 | + if get_fortune() != nil and PUtils::check_opts_for_cmd(opts) | |
69 | + @mail_error = NO_ERROR | |
70 | + else | |
71 | + @mail_error = ERROR | |
72 | + @mail_error_msg = "実行可能な fortune プログラムファイルが設定されていません" | |
73 | + end | |
74 | + end | |
75 | + | |
76 | + | |
77 | + def get_mail_body(mail, opts, mode) | |
78 | + bin = get_fortune() | |
79 | + | |
80 | + # opts が有効な値ならばそれを使う | |
81 | + body = (opts == nil or opts =~ /^\s*$/) ? | |
82 | + `#{bin} #{@@FORTUNE_OPTS} 2>&1` : | |
83 | + `#{bin} #{opts} 2>&1` | |
84 | + | |
85 | + st = $? | |
86 | + if st == nil | |
87 | + return 'fortune コマンドの終了ステータス取得に失敗しました。' | |
88 | + end | |
89 | + | |
90 | + if st.exitstatus != 0 | |
91 | + err = "fortune コマンドは正常な値を返しませんでした。\n" | |
92 | + err << "戻り値: #{ret.ret.exitstatus} \n" | |
93 | + err << "出力: #{body}" | |
94 | + return err | |
95 | + end | |
96 | + | |
97 | + return body | |
98 | + end | |
99 | +end | |
100 | + | |
101 | +__END__ |
@@ -0,0 +1,51 @@ | ||
1 | +# | |
2 | +# システムの状態情報を返す | |
3 | +# | |
4 | +# $Id$ | |
5 | +# | |
6 | +$KCODE = 'e' | |
7 | +require "#{PLUGIN_DIR}/plugin.rb" | |
8 | +require "#{LIB_DIR}/utils.rb" | |
9 | + | |
10 | +class Plugin_status < Plugin | |
11 | + def Plugin_status.use_this_class? ; true ; end | |
12 | + def Plugin_status.use_thread? ; false ; end | |
13 | + | |
14 | + def Plugin_status.purpose ; 'SimRM のシステム情報を取得' ; end | |
15 | + def Plugin_status.author ; 'isr' ; end | |
16 | + def Plugin_status.version ; '$Id$' ; end | |
17 | + | |
18 | + def Plugin_status.help | |
19 | + body = <<_EOS_ | |
20 | +SimRM のシステム情報、ステータスを取得します。 | |
21 | + | |
22 | +Usage: | |
23 | + plugin:status | |
24 | + | |
25 | +_EOS_ | |
26 | + return body | |
27 | + end | |
28 | + | |
29 | + | |
30 | + def initialize(mail, opts, mode) | |
31 | + @mail_error = NO_ERROR | |
32 | + end | |
33 | + | |
34 | + | |
35 | + def get_mail_body(mail, opts, mode) | |
36 | + body = "=====================================================================\n" | |
37 | + body += "System 'uptime':\n" | |
38 | + body += `uptime` | |
39 | + body += "TODO: SimRM's 'uptime'\n" | |
40 | + body += "=====================================================================\n" | |
41 | + body += "- 保存メール数: #{Utils::dir_filenum(RESERVED_DIR)}\n" | |
42 | + body += "- 保存メモ数: #{Utils::dir_filenum(MEMO_DIR)}\n" | |
43 | + | |
44 | + s, cnt = Utils::dir_filesize(LOG_DIR) | |
45 | + body += "- ログの数とサイズ: 全 #{cnt} ファイル、合計 #{s} バイト\n" | |
46 | + body += "=====================================================================\n" | |
47 | + return body | |
48 | + end | |
49 | +end | |
50 | + | |
51 | +__END__ |
@@ -0,0 +1,55 @@ | ||
1 | +# | |
2 | +# MD5 値を返す | |
3 | +# | |
4 | +# 以下のケースで利用 | |
5 | +# - 各プラグインの PASSWORD に設定する MD5 値を求める時 | |
6 | +# - MD5 の計算環境が手元に無い時 | |
7 | +# | |
8 | +# $Id$ | |
9 | +# | |
10 | + | |
11 | +$KCODE = 'e' | |
12 | +require "#{PLUGIN_DIR}/plugin.rb" | |
13 | +require "#{PLUGIN_DIR}/plugin_utils.rb" | |
14 | + | |
15 | + | |
16 | +class Plugin_md5 < Plugin | |
17 | + def Plugin_md5.use_this_class? ; true ; end | |
18 | + def Plugin_md5.use_thread? ; false ; end | |
19 | + | |
20 | + def Plugin_md5.purpose ; 'メール本文の MD5 値を求める' ; end | |
21 | + def Plugin_md5.author ; 'isr' ; end | |
22 | + def Plugin_md5.version ; '$Id$' ; end | |
23 | + | |
24 | + def Plugin_md5.help | |
25 | + body = <<_EOS_ | |
26 | +メール本文の MD5 値を求めます。 | |
27 | +パスワードとして設定する MD5 値を求める時などに使用してください。 | |
28 | + | |
29 | +Usage: | |
30 | + plugin:md5 | |
31 | + | |
32 | +制限: | |
33 | + - メール本文先頭と末尾の空白文字列は削除されます | |
34 | + | |
35 | +_EOS_ | |
36 | + return body | |
37 | + end | |
38 | + | |
39 | + | |
40 | + def initialize(mail, opts, mode) | |
41 | + err, msg = PUtils::check_body_for_md5(mail.body, PUtils::MD5) | |
42 | + @mail_error = err | |
43 | + @mail_error_msg = msg | |
44 | + end | |
45 | + | |
46 | + | |
47 | + # NOTE: 先頭と末尾の空白文字列は削除する | |
48 | + def get_mail_body(mail, opts, mode) | |
49 | + body = "下記があなたのメール本文から生成された MD5 値となります。\n" | |
50 | + body << PUtils::get_md5(mail.body.to_s) | |
51 | + return body | |
52 | + end | |
53 | +end | |
54 | + | |
55 | +__END__ |
@@ -0,0 +1,129 @@ | ||
1 | +# | |
2 | +# 引数として与えられたファイルの内容を返す | |
3 | +# cat との違い: ファイルが無い場合にエラーメールを送信しない | |
4 | +# | |
5 | +# インストール方法: | |
6 | +# 1. コマンドで echo -n "YOUR_PASSWORD" | md5sum を実行し、 | |
7 | +# その MD5 値をこのファイルの PASSWORD に設定する | |
8 | +# 2. このファイルの use_this_class? を true にする | |
9 | +# | |
10 | +# $Id$ | |
11 | +# | |
12 | + | |
13 | +$KCODE = 'e' | |
14 | +require "#{PLUGIN_DIR}/plugin.rb" | |
15 | +require "#{PLUGIN_DIR}/plugin_utils.rb" | |
16 | + | |
17 | +require 'nkf' | |
18 | + | |
19 | +# NOTE: 再読込できるように定数は定義しない | |
20 | +class Plugin_ifcat < Plugin | |
21 | + # このプラグインを使う時に必要なパスワード値 | |
22 | + @@PASSWORD = '' # TODO: 適切なパスワードを用いて、MD5 値を記入すること | |
23 | + #@@PASSWORD = '25d55ad283aa400af464c76d713c07ad' # '12345678' の MD5 値 | |
24 | + #@@PASSWORD = 'ea703e7aa1efda0064eaa507d9e8ab7e' # 'hoge' の MD5 値 | |
25 | + | |
26 | + # 結果を返すにあたり、許可できるサイズ上限 | |
27 | + @@MAX_SIZE = 100 * 1024 | |
28 | + | |
29 | + def Plugin_ifcat.use_this_class? ; false ; end | |
30 | + def Plugin_ifcat.use_thread? ; false ; end | |
31 | + def Plugin_ifcat.purpose ; 'このサーバ上に該当ファイルがあれば、そのファイル内容を返します' ; end | |
32 | + def Plugin_ifcat.author ; 'isr' ; end | |
33 | + def Plugin_ifcat.version ; '$Id$' ; end | |
34 | + | |
35 | + def Plugin_ifcat.help | |
36 | + | |
37 | + body = <<_EOS_ | |
38 | +SimRM を実行しているマシン上のファイルにアクセスし、そのファイル内容を返します。 | |
39 | +cat プラグインと違い、ファイルが無い場合にエラーメールを送信しません。 | |
40 | + | |
41 | +Usage: | |
42 | + plugin:ifcat:FILE_NAME | |
43 | + | |
44 | + メールの件名に対象ファイルを記述し、メール本文にはパスワードを記述してください。 | |
45 | + | |
46 | +制限: | |
47 | + - 複数ファイルを引数に取ることは出来ません。 | |
48 | + | |
49 | +_EOS_ | |
50 | + return body | |
51 | + end | |
52 | + | |
53 | + | |
54 | + # 次の場合はエラーとする。 | |
55 | + # - メール本文のパスワードが異なるとき | |
56 | + # - 引数がファイルを指していないとき | |
57 | + # - バイナリファイルを cat しようとしているとき | |
58 | + # - ファイルサイズが MAX_SIZE を越えるとき | |
59 | + def initialize(mail, opts, mode) | |
60 | + # パスワードチェック | |
61 | + @mail_error, @mail_error_msg = PUtils::check_body_for_md5(mail.body, PUtils::PASSWORD) | |
62 | + return if @mail_error != NO_ERROR | |
63 | + | |
64 | + if PUtils::get_md5(mail.body.to_s) != @@PASSWORD | |
65 | + @mail_error = ERROR | |
66 | + @mail_error_msg = "パスワードが異なります" | |
67 | + return | |
68 | + end | |
69 | + | |
70 | + # 保存時はファイルチェックを行わない | |
71 | + return if mode == MODE_SAVE | |
72 | + | |
73 | + # エラーチェックが多いため | |
74 | + # @mail_error は基本 ERROR にしておく | |
75 | + @mail_error = ERROR | |
76 | + | |
77 | + # 対象ファイルチェック | |
78 | + if opts == nil or opts == "" | |
79 | + @mail_error_msg = '対象ファイルが記述されていません' | |
80 | + return | |
81 | + end | |
82 | + unless File.file?(opts) | |
83 | + @mail_error = IGNORE_ME | |
84 | + @mail_error_msg = '' | |
85 | + return | |
86 | + end | |
87 | + unless File.readable?(opts) | |
88 | + @mail_error_msg = "ファイル '#{opts}' の読み込み権限がありません" | |
89 | + return | |
90 | + end | |
91 | + if File.zero?(opts) | |
92 | + @mail_error_msg = "ファイル '#{opts}' は空ファイルです" | |
93 | + return | |
94 | + end | |
95 | + | |
96 | + # サイズチェック | |
97 | + s = File.size(opts) | |
98 | + if s > @@MAX_SIZE | |
99 | + @mail_error_msg = "ファイル '#{opts}' のサイズが上限 #{@@MAX_SIZE} を超えた #{s} です" | |
100 | + return | |
101 | + end | |
102 | + | |
103 | + # バイナリファイルかどうか? | |
104 | + begin | |
105 | + str = open(opts) {|io| io.gets(nil) } | |
106 | + | |
107 | + case NKF.guess(str) | |
108 | + when NKF::BINARY | |
109 | + @mail_error_msg = "ファイル '#{opts}' はバイナリファイルです" | |
110 | + return | |
111 | + when NKF::UNKNOWN | |
112 | + @mail_error_msg = "ファイル '#{opts}' の文字コード判別に失敗しました" | |
113 | + return | |
114 | + end | |
115 | + rescue | |
116 | + @mail_error_msg = "ファイル '#{opts}' のチェック中に例外が発生しました" | |
117 | + return | |
118 | + end | |
119 | + | |
120 | + @mail_error = NO_ERROR | |
121 | + end | |
122 | + | |
123 | + | |
124 | + def get_mail_body(mail, opts, mode) | |
125 | + return open(opts) {|io| io.gets(nil) } | |
126 | + end | |
127 | +end | |
128 | + | |
129 | +__END__ |
@@ -0,0 +1,95 @@ | ||
1 | +# | |
2 | +# プラグインを reload() する | |
3 | +# | |
4 | +# 以下のケースで利用 | |
5 | +# - use_this_class? の値を変更したプラグインを再読込したいとき | |
6 | +# - 新しいプラグインを PLUGIN_DIR に配置したので、それを読み込みたいとき | |
7 | +# - 新しい PLUGIN_DIR を利用したいとき (引数にそれを指定する) | |
8 | +# | |
9 | +# インストール方法: | |
10 | +# 1. コマンドで echo -n "YOUR_PASSWORD" | md5sum を実行し、 | |
11 | +# その MD5 値をこのファイルの PASSWORD に設定する | |
12 | +# 2. このファイルの use_this_class? を true にする | |
13 | +# | |
14 | +# $Id$ | |
15 | +# | |
16 | + | |
17 | +$KCODE = 'e' | |
18 | +require "#{LIB_DIR}/pmanager.rb" | |
19 | +require "#{PLUGIN_DIR}/plugin.rb" | |
20 | +require "#{PLUGIN_DIR}/plugin_utils.rb" | |
21 | + | |
22 | +class Plugin_reload < Plugin | |
23 | + # このプラグインを使う時に必要なパスワード値 | |
24 | + @@PASSWORD = '' # TODO: 適切なパスワードを用いて、MD5 値を記入すること | |
25 | + | |
26 | + def Plugin_reload.use_this_class? ; false ; end | |
27 | + def Plugin_reload.use_thread? ; false ; end | |
28 | + | |
29 | + def Plugin_reload.purpose ; 'プラグインの内容及び状態変更時に再読込を実行させる' ; end | |
30 | + def Plugin_reload.author ; 'isr' ; end | |
31 | + def Plugin_reload.version ; '$Id$' ; end | |
32 | + | |
33 | + def Plugin_reload.help | |
34 | + body = <<_EOS_ | |
35 | +各プラグインの内容変更及び状態変更時、SimRM に再読込を行わせるプラグイン。 | |
36 | + | |
37 | +Usage: | |
38 | + plugin:reload[:PLUGIN_DIR] | |
39 | + | |
40 | + - メール本文にはパスワードを記述してください。 | |
41 | + - 新しく参照するプラグインディレクトリがある場合は PLUGIN_DIR 引数を指定してください。 | |
42 | + | |
43 | +_EOS_ | |
44 | + return body | |
45 | + end | |
46 | + | |
47 | + | |
48 | + # true -> OK | |
49 | + def check_opts(opts) | |
50 | + return File.directory?(opts) if opts != nil and opts != "" | |
51 | + return true | |
52 | + end | |
53 | + | |
54 | + | |
55 | + def initialize(mail, opts, mode) | |
56 | + # パスワードチェック | |
57 | + @mail_error, @mail_error_msg = PUtils::check_body_for_md5(mail.body, PUtils::PASSWORD) | |
58 | + return if @mail_error != NO_ERROR | |
59 | + # 生成した MD5 が等しいか? | |
60 | + if PUtils::get_md5(mail.body.to_s) != @@PASSWORD | |
61 | + @mail_error = ERROR | |
62 | + @mail_error_msg = "パスワードが異なります" | |
63 | + return | |
64 | + end | |
65 | + | |
66 | + unless check_opts(opts) | |
67 | + @mail_error = ERROR | |
68 | + @mail_error_msg = "'#{opts}' はディレクトリではありません" | |
69 | + return | |
70 | + end | |
71 | + | |
72 | + @mail_error = NO_ERROR | |
73 | + end | |
74 | + | |
75 | + | |
76 | + def get_mail_body(mail, opts, mode) | |
77 | + p = Pmanager.instance | |
78 | + | |
79 | + body = "reload() 実行前に利用可能なプラグインは次のとおりです。\n" | |
80 | + p.get_plugin_names().each {|name| body << "- #{name}\n" } | |
81 | + body << "\n" | |
82 | + | |
83 | + ret = p.reload((check_opts(opts)) ? opts : "") | |
84 | + if ret != 0 | |
85 | + return "reload() に失敗しました: 戻り値 #{ret}\n\n" + body | |
86 | + end | |
87 | + | |
88 | + body << "reload() 実行後の現在、利用可能なプラグインは次のとおりです。\n" | |
89 | + p.get_plugin_names().each {|name| body << "- #{name}\n" } | |
90 | + | |
91 | + return body | |
92 | + end | |
93 | +end | |
94 | + | |
95 | +__END__ |
@@ -0,0 +1,125 @@ | ||
1 | +# | |
2 | +# 引数として与えられたファイルの内容を返す | |
3 | +# | |
4 | +# インストール方法: | |
5 | +# 1. コマンドで echo -n "YOUR_PASSWORD" | md5sum を実行し、 | |
6 | +# その MD5 値をこのファイルの PASSWORD に設定する | |
7 | +# 2. このファイルの use_this_class? を true にする | |
8 | +# | |
9 | +# $Id$ | |
10 | +# | |
11 | + | |
12 | +$KCODE = 'e' | |
13 | +require "#{PLUGIN_DIR}/plugin.rb" | |
14 | +require "#{PLUGIN_DIR}/plugin_utils.rb" | |
15 | + | |
16 | +require 'nkf' | |
17 | + | |
18 | +# NOTE: 再読込できるように定数は定義しない | |
19 | +class Plugin_cat < Plugin | |
20 | + # このプラグインを使う時に必要なパスワード値 | |
21 | + @@PASSWORD = '' # TODO: 適切なパスワードを用いて、MD5 値を記入すること | |
22 | + #@@PASSWORD = '25d55ad283aa400af464c76d713c07ad' # '12345678' の MD5 値 | |
23 | + #@@PASSWORD = 'ea703e7aa1efda0064eaa507d9e8ab7e' # 'hoge' の MD5 値 | |
24 | + | |
25 | + # 結果を返すにあたり、許可できるサイズ上限 | |
26 | + @@MAX_SIZE = 100 * 1024 | |
27 | + | |
28 | + def Plugin_cat.use_this_class? ; false ; end | |
29 | + def Plugin_cat.use_thread? ; false ; end | |
30 | + def Plugin_cat.purpose ; 'このサーバ上のファイルにアクセスし、そのファイル内容を返します' ; end | |
31 | + def Plugin_cat.author ; 'isr' ; end | |
32 | + def Plugin_cat.version ; '$Id$' ; end | |
33 | + | |
34 | + def Plugin_cat.help | |
35 | + | |
36 | + body = <<_EOS_ | |
37 | +SimRM を実行しているマシン上のファイルにアクセスし、そのファイル内容を返します。 | |
38 | + | |
39 | +Usage: | |
40 | + plugin:cat:FILE_NAME | |
41 | + | |
42 | + メールの件名に対象ファイルを記述し、メール本文にはパスワードを記述してください。 | |
43 | + | |
44 | +制限: | |
45 | + - 複数ファイルを引数に取ることは出来ません。 | |
46 | + | |
47 | +_EOS_ | |
48 | + return body | |
49 | + end | |
50 | + | |
51 | + | |
52 | + # true -> OK | |
53 | + def check_opts(opts) | |
54 | + return false if opts == nil or opts == "" | |
55 | + return File.file?(opts) | |
56 | + end | |
57 | + | |
58 | + | |
59 | + # 次の場合はエラーとする。 | |
60 | + # - メール本文のパスワードが異なるとき | |
61 | + # - 引数がファイルを指していないとき | |
62 | + # - バイナリファイルを cat しようとしているとき | |
63 | + # - ファイルサイズが MAX_SIZE を越えるとき | |
64 | + def initialize(mail, opts, mode) | |
65 | + # パスワードチェック | |
66 | + @mail_error, @mail_error_msg = PUtils::check_body_for_md5(mail.body, PUtils::PASSWORD) | |
67 | + return if @mail_error != NO_ERROR | |
68 | + | |
69 | + # エラーチェックが多いため | |
70 | + # @mail_error は基本 ERROR にしておく | |
71 | + @mail_error = ERROR | |
72 | + | |
73 | + if PUtils::get_md5(mail.body.to_s) != @@PASSWORD | |
74 | + @mail_error_msg = "パスワードが異なります" | |
75 | + return | |
76 | + end | |
77 | + | |
78 | + # 対象ファイルチェック | |
79 | + unless check_opts(opts) | |
80 | + @mail_error_msg = "'#{opts}' はファイルではありません" | |
81 | + return | |
82 | + end | |
83 | + unless File.readable?(opts) | |
84 | + @mail_error_msg = "ファイル '#{opts}' の読み込み権限がありません" | |
85 | + return | |
86 | + end | |
87 | + if File.zero?(opts) | |
88 | + @mail_error_msg = "ファイル '#{opts}' は空ファイルです" | |
89 | + return | |
90 | + end | |
91 | + | |
92 | + # サイズチェック | |
93 | + s = File.size(opts) | |
94 | + if s > @@MAX_SIZE | |
95 | + @mail_error_msg = "ファイル '#{opts}' のサイズが上限 #{@@MAX_SIZE} を超えた #{s} です" | |
96 | + return | |
97 | + end | |
98 | + | |
99 | + # バイナリファイルかどうか? | |
100 | + begin | |
101 | + str = open(opts) {|io| io.gets(nil) } | |
102 | + | |
103 | + case NKF.guess(str) | |
104 | + when NKF::BINARY | |
105 | + @mail_error_msg = "ファイル '#{opts}' はバイナリファイルです" | |
106 | + return | |
107 | + when NKF::UNKNOWN | |
108 | + @mail_error_msg = "ファイル '#{opts}' の文字コード判別に失敗しました" | |
109 | + return | |
110 | + end | |
111 | + rescue | |
112 | + @mail_error_msg = "ファイル '#{opts}' のチェック中に例外が発生しました" | |
113 | + return | |
114 | + end | |
115 | + | |
116 | + @mail_error = NO_ERROR | |
117 | + end | |
118 | + | |
119 | + | |
120 | + def get_mail_body(mail, opts, mode) | |
121 | + return open(opts) {|io| io.gets(nil) } | |
122 | + end | |
123 | +end | |
124 | + | |
125 | +__END__ |
@@ -0,0 +1,4 @@ | ||
1 | +プラグインを作成するときは、make_plugin.rb を実行することで | |
2 | +ベースファイルが作成できます。plugin.rb と sample.smplg.rb を | |
3 | +参考にしてベースファイルを編集することで、少しは楽に作成でき | |
4 | +ると思います。 |
@@ -0,0 +1,118 @@ | ||
1 | +# | |
2 | +# help プラグイン。 | |
3 | +# 各プラグインの目的、使い方を返します。 | |
4 | +# | |
5 | +# $Id$ | |
6 | +# | |
7 | + | |
8 | +$KCODE = 'e' | |
9 | +require "#{LIB_DIR}/pmanager.rb" | |
10 | +require "#{PLUGIN_DIR}/plugin.rb" | |
11 | + | |
12 | +class Plugin_help < Plugin | |
13 | + def Plugin_help.use_this_class? ; true ; end | |
14 | + def Plugin_help.use_thread? ; false ; end | |
15 | + def Plugin_help.purpose ; '各プラグインの目的、使い方を返す' ; end | |
16 | + def Plugin_help.author ; 'isr' ; end | |
17 | + def Plugin_help.version ; '$Id$' ; end | |
18 | + | |
19 | + def Plugin_help.help | |
20 | + body = <<_EOS_ | |
21 | +ここでは SimRM のプラグイン利用方法を示したのち、この help プラグインの使い方を示します。 | |
22 | + | |
23 | +[SimRM プラグイン利用方法] | |
24 | + | |
25 | +メールの件名を次のフォーマットにすることで各プラグインの機能が利用できます。 | |
26 | + | |
27 | +== ここから =========================================== | |
28 | +plugin:PLUGIN_NAME[:PLUGIN_OPTIONS] # plugin: は plg: も可 | |
29 | + | |
30 | +YYYYMMDDHHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
31 | +YYMMDDHHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
32 | +MMDDHHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
33 | +DDHHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
34 | +HHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
35 | +HH:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
36 | +everyhour:mm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
37 | +everyday:HHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
38 | +everyweek:wHHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] # w には 0〜6 の値が入り、0 が日曜日 | |
39 | +everymonth:DDHHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
40 | +everyyear:MMDDHHmm:PLUGIN_NAME[:PLUGIN_OPTIONS] | |
41 | +== ここまで =========================================== | |
42 | + | |
43 | +e.g. | |
44 | + plugin:cat:foo_file | |
45 | + 10:ls:/var/log | |
46 | + everyday:0800:ifcat:foo_file | |
47 | + | |
48 | +メモ: | |
49 | + - どのプラグインが使えるか知りたければ list プラグインを使ってください。 | |
50 | + (ただし、list プラグインが使えないこともあるかもしれません) | |
51 | + | |
52 | + - プラグインではなく SimRM システム全体の使い方を知りたい場合は | |
53 | + help という件名でメールを送ってください。 | |
54 | + (件名先頭に plugin: を記述しないこと) | |
55 | + | |
56 | + | |
57 | + | |
58 | +[help プラグインについて] | |
59 | + | |
60 | +SimRM が持つ各プラグインの目的、使い方を返します。 | |
61 | + | |
62 | +Usage: | |
63 | + plugin:help[:PLUGIN_NAME] | |
64 | + | |
65 | +e.g. | |
66 | + plugin:help | |
67 | + plugin:help:status | |
68 | + | |
69 | +Tips: | |
70 | + - PLUGIN_NAME 引数が無い場合は、この help プラグインのヘルプ情報を返します。 | |
71 | + | |
72 | +_EOS_ | |
73 | + return body | |
74 | + end | |
75 | + | |
76 | + | |
77 | + # OK -> true | |
78 | + def check_opts(opts) | |
79 | + return Pmanager.instance.include?(opts) | |
80 | + end | |
81 | + | |
82 | + | |
83 | + def initialize(mail, opts, mode) | |
84 | + @mail_error = NO_ERROR | |
85 | + @mail_error_msg = 'no error' | |
86 | + | |
87 | + return if opts == nil or opts == '' | |
88 | + | |
89 | + unless check_opts(opts) | |
90 | + @mail_error = ERROR | |
91 | + @mail_error_msg = "プラグイン '#{opts}' は存在しません。" | |
92 | + @mail_error_msg += '(このプラグインの使い方を知るには plugin:help という件名でメールを送ってください)' | |
93 | + end | |
94 | + end | |
95 | + | |
96 | + | |
97 | + def get_mail_body(mail, opts, mode) | |
98 | + opts = 'help' if opts == nil or opts == '' | |
99 | + | |
100 | + unless check_opts(opts) | |
101 | + return "プラグイン '#{opts}' は存在しません。" | |
102 | + end | |
103 | + | |
104 | + c = eval("Plugin_#{opts}") | |
105 | + | |
106 | + body = '' | |
107 | + body << "==============================================\n" | |
108 | + body << "'#{opts}' プラグインのヘルプ情報です\n" | |
109 | + body << "==============================================\n" | |
110 | + body << (c.help rescue "'#{opts}' プラグインにはヘルプ情報が記載されていません\n") | |
111 | + body << "==============================================\n" | |
112 | + body << Plugin.signature(c) | |
113 | + body << "==============================================\n" | |
114 | + return body | |
115 | + end | |
116 | +end | |
117 | + | |
118 | +__END__ |
@@ -0,0 +1,41 @@ | ||
1 | +#!/usr/bin/env ruby | |
2 | +# | |
3 | +# You can create a plugin base file with this. | |
4 | +# | |
5 | +# $Id$ | |
6 | +# | |
7 | + | |
8 | +DAT = File.dirname($0) + '/make_plugin.dat' | |
9 | + | |
10 | +name = '' | |
11 | +fname = '' | |
12 | +pname = '' | |
13 | +loop { | |
14 | + print "Plugin name? []: " | |
15 | + name = gets | |
16 | + name.chomp! | |
17 | + if name =~ /^\s*$/ or name =~ /[^0-9a-z_]/ | |
18 | + STDERR.puts "Your input is invalid. So, try again." | |
19 | + next | |
20 | + end | |
21 | + | |
22 | + fname = "#{name}.smplg.rb" | |
23 | + pname = "Plugin_#{name}" | |
24 | + | |
25 | + if File.exist?(fname) | |
26 | + STDERR.puts "'#{fname}' already exists. So, input the other name." | |
27 | + next | |
28 | + end | |
29 | + | |
30 | + puts "OK, your plugin name is '#{pname}'." | |
31 | + break | |
32 | +} | |
33 | + | |
34 | +f = open("#{fname}", 'w') | |
35 | +File.foreach(DAT) {|l| | |
36 | + f.print l.sub(/_PLUGIN_NAME_/, pname) | |
37 | +} | |
38 | +f.close | |
39 | + | |
40 | +puts "A base file '#{fname}' was prepared." | |
41 | +puts "Enjoy it!" |
@@ -0,0 +1,57 @@ | ||
1 | +# | |
2 | +# 現在利用可能なプラグインのリストを表示する | |
3 | +# | |
4 | +# $Id$ | |
5 | +# | |
6 | + | |
7 | +$KCODE = 'e' | |
8 | +require "#{LIB_DIR}/pmanager.rb" | |
9 | +require "#{PLUGIN_DIR}/plugin.rb" | |
10 | + | |
11 | +class Plugin_list < Plugin | |
12 | + def Plugin_list.use_this_class? ; true ; end | |
13 | + def Plugin_list.use_thread? ; false ; end | |
14 | + | |
15 | + def Plugin_list.purpose ; '現在利用可能なプラグインのリストを表示する' ; end | |
16 | + def Plugin_list.author ; 'isr' ; end | |
17 | + def Plugin_list.version ; '$Id$' ; end | |
18 | + | |
19 | + def Plugin_list.help | |
20 | + body = <<_EOS_ | |
21 | +現在利用可能なプラグインのリストを返し、次の情報が記載されます。 | |
22 | +- プラグイン名 | |
23 | +- 作者 | |
24 | +- バージョン | |
25 | + | |
26 | +Usage: | |
27 | + plugin:list | |
28 | + | |
29 | +_EOS_ | |
30 | + return body | |
31 | + end | |
32 | + | |
33 | + | |
34 | + def initialize(mail, opts, mode) | |
35 | + @mail_error = NO_ERROR | |
36 | + end | |
37 | + | |
38 | + | |
39 | + def get_mail_body(mail, opts, mode) | |
40 | + err = 'Error: Not Defined' | |
41 | + body = "現在、利用可能なプラグインは次のとおりです。\n" | |
42 | + | |
43 | + p = Pmanager.instance | |
44 | + p.get_plugin_names().each {|name| | |
45 | + c = eval("Plugin_#{name}") | |
46 | + | |
47 | + body << "- #{name}\n" | |
48 | + body << " Purpose: #{defined?(c.purpose) ? c.purpose : err}\n" | |
49 | + body << " Author: #{defined?(c.author) ? c.author : err}\n" | |
50 | + body << " Version: #{defined?(c.version) ? c.version : err}\n" | |
51 | + body << "\n" | |
52 | + } | |
53 | + return body | |
54 | + end | |
55 | +end | |
56 | + | |
57 | +__END__ |
@@ -0,0 +1,29 @@ | ||
1 | +/* | |
2 | + * Copyright (c) 2007 isr | |
3 | + * All rights reserved. | |
4 | + * | |
5 | + * This software including the files in this directory is provided under | |
6 | + * the following license. | |
7 | + * | |
8 | + * Redistribution and use in source and binary forms, with or without | |
9 | + * modification, are permitted provided that the following conditions | |
10 | + * are met: | |
11 | + * 1. Redistributions of source code must retain the above copyright | |
12 | + * notice, this list of conditions and the following disclaimer. | |
13 | + * 2. Redistributions in binary form must reproduce the above copyright | |
14 | + * notice, this list of conditions and the following disclaimer in the | |
15 | + * documentation and/or other materials provided with the distribution. | |
16 | + * | |
17 | + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND | |
18 | + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
19 | + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
20 | + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE | |
21 | + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
22 | + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |
23 | + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
24 | + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |
25 | + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |
26 | + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |
27 | + * SUCH DAMAGE. | |
28 | + */ | |
29 | + |
@@ -0,0 +1,246 @@ | ||
1 | +#!/usr/bin/env ruby | |
2 | +# | |
3 | +# SMTP Relay With ISP | |
4 | +# sendmail が携帯ドメインにメールを送るためのエージェントツール | |
5 | +# | |
6 | +# Usage: | |
7 | +# cat mail | smisp | |
8 | +# | |
9 | +# 標準入力から読み込んだメールを指定された SMTP サーバを使って配送します。 | |
10 | +# SMTP サーバなどを設定するには、このファイルを直接編集してください。 | |
11 | +# | |
12 | +# $Id$ | |
13 | +# | |
14 | + | |
15 | +$KCODE = 'e' # !重要! ファイルの文字コードを変更する際はここも変更すること | |
16 | + | |
17 | +require 'mailread' | |
18 | +require 'net/smtp' | |
19 | + | |
20 | + | |
21 | +#---------------------------------------------------------- | |
22 | +# ユーザの設定が必要な箇所 | |
23 | +#---------------------------------------------------------- | |
24 | + | |
25 | +# メールを送信するときに使う SMTP サーバとポート番号 | |
26 | +# NOTE: Yahoo BB なら ybbsmtp.mail.yahoo.co.jp | |
27 | +SMTP_HOST = '192.168.0.254' | |
28 | +SMTP_PORT = 25 | |
29 | +# アカウントのメールアドレスを記述してください | |
30 | +# NOTE: SMTP サーバに MAIL FROM 命令を送る際、利用する | |
31 | +SMTP_FROM = '' | |
32 | +# SMTP にログインの必要があれば、ユーザ名、パスワードを設定 | |
33 | +# NOTE: 必要が無ければ nil を設定してください | |
34 | +SMTP_USER = nil | |
35 | +SMTP_PASS = nil | |
36 | +# ログイン認証方法を記してください (nil, :login, :plain, :cram_md5) | |
37 | +# NOTE: Yahoo BB は :plain | |
38 | +SMTP_AUTH = :plain | |
39 | +# わからなければ特に指定しなくていいと思います | |
40 | +SMTP_HELO = 'localhost.localdomain' | |
41 | + | |
42 | +# ログ | |
43 | +# NOTE: デフォルトの '/var/log/smisp' は make 時に作成されます | |
44 | +# See: Makefile | |
45 | +LOG_PATH = '/var/log/smisp' | |
46 | + | |
47 | +# 送ってもいいメールアドレスを記述してください | |
48 | +WHITE_LIST = [ | |
49 | + # 例: | |
50 | + # 'fooooo01@ezweb.ne.jp', | |
51 | + # 'fooooo02@ezweb.ne.jp' | |
52 | + '' | |
53 | +] | |
54 | + | |
55 | + | |
56 | +#---------------------------------------------------------- | |
57 | + | |
58 | +# Message-ID, To, From 情報を持つ | |
59 | +# 最後のログに必要 | |
60 | +class MailLogInfo | |
61 | + attr_accessor :msgid, :to, :from | |
62 | + def initialize(msgid, to, from) | |
63 | + @msgid = msgid | |
64 | + @to = to | |
65 | + @from = from | |
66 | + end | |
67 | + | |
68 | + | |
69 | + # 渡された MailLogInfo のリストに自分と同じ情報があるか | |
70 | + def exist?(list) | |
71 | + list.each { |i| | |
72 | + return true if i.msgid == @msgid and i.to == @to and i.from == @from | |
73 | + } | |
74 | + return false | |
75 | + end | |
76 | +end | |
77 | + | |
78 | + | |
79 | +#---------------------------------------------------------- | |
80 | +# 以下、処理 | |
81 | +#---------------------------------------------------------- | |
82 | + | |
83 | +def usage(argc) | |
84 | + if argc != 0 | |
85 | + STDERR.print <<'EndOfUsage' | |
86 | +使用法: cat mail | smisp.rb | |
87 | + | |
88 | +標準入力から読み込んだメールを指定された SMTP サーバを使って配送します。 | |
89 | +SMTP サーバなどを設定するには、このファイルを直接編集してください。 | |
90 | + | |
91 | +EndOfUsage | |
92 | + return 10 | |
93 | + end | |
94 | + | |
95 | + return 0 | |
96 | +end | |
97 | + | |
98 | + | |
99 | +# 各種チェック | |
100 | +def main_check() | |
101 | + ret = usage(ARGV.length) | |
102 | + return ret if ret != 0 | |
103 | + | |
104 | + # 開けるかチェック | |
105 | + begin | |
106 | + f = File.open(LOG_PATH, 'a') | |
107 | + rescue | |
108 | + STDERR.puts "ERROR: Can't open a log file #{LOG_PATH}" | |
109 | + return 20 | |
110 | + ensure | |
111 | + f.close if f | |
112 | + end | |
113 | + | |
114 | + return 0 | |
115 | +end | |
116 | + | |
117 | + | |
118 | +# addr にメールを送っていいかどうかを判定します | |
119 | +# | |
120 | +# TODO: 文字列中に指定文字列があるか確認しているだけなので判定が甘い | |
121 | +# 仕様の検討が必要と思われる | |
122 | +def send_ok?(addr) | |
123 | + # 空文字列は送ってはいけない | |
124 | + return false if addr.empty? | |
125 | + | |
126 | + # ホワイトリストが空ならば、全てのアドレスを無効とする | |
127 | + return false if WHITE_LIST == [] | |
128 | + | |
129 | + # ホワイトリストに載っているか? | |
130 | + WHITE_LIST.each do |w| | |
131 | + return true if w != '' and addr.index(w) | |
132 | + end | |
133 | + | |
134 | + return false | |
135 | +end | |
136 | + | |
137 | + | |
138 | +# メールヘッダのフィールドからメールアドレスを取得し、 | |
139 | +# そのリストを返す | |
140 | +# NOTE: str に改行があっても大丈夫 | |
141 | +# TODO: かなり簡易的 | |
142 | +def make_address_list(str) | |
143 | + return [] unless str | |
144 | + return str.scan(/[^<,\s]+@[^>,\s]+/) | |
145 | +end | |
146 | + | |
147 | + | |
148 | +# Mail クラスから元のメールを再構築する | |
149 | +def make_contents(mail) | |
150 | + contents = "" | |
151 | + return contents unless mail | |
152 | + | |
153 | + mail.header.each { |k, v| | |
154 | + contents += "#{k}: " + v.gsub(/\n/, "\n ") + "\n" | |
155 | + } | |
156 | + contents += "\n" + mail.body.to_s | |
157 | +end | |
158 | + | |
159 | + | |
160 | +# メール送信処理 | |
161 | +# id_list, ng_list は更新される | |
162 | +def send_mail!(mail, id_list, ng_list) | |
163 | + # To, Cc, Bcc 全ての宛先を取得 | |
164 | + # TODO: Bcc ヘッダーは削除されてしまうため取得できず、中継できない | |
165 | + ads = [] | |
166 | + ads += make_address_list(mail['to']) | |
167 | + ads += make_address_list(mail['cc']) | |
168 | + ads += make_address_list(mail['bcc']) | |
169 | + | |
170 | + sm = nil | |
171 | + msgid = mail['message-id'] ? mail['message-id'].gsub(/\n/, ' ') : '' | |
172 | + from = mail['from'] ? mail['from'].gsub(/\n/, ' ') : '' | |
173 | + | |
174 | + ads.each { |ad| | |
175 | + # 送信先は WHITE_LIST に載っているか? | |
176 | + next unless send_ok?(ad) | |
177 | + | |
178 | + # 送信済みのメールではないか? | |
179 | + minfo = MailLogInfo.new(msgid, ad, from) | |
180 | + next if minfo.exist?(id_list) | |
181 | + id_list << minfo | |
182 | + | |
183 | + # 以下、送信処理に移るが、 | |
184 | + # セッションがまだ張られていなければここで張る | |
185 | + if sm == nil | |
186 | + sm = Net::SMTP.start(SMTP_HOST, SMTP_PORT, | |
187 | + SMTP_HELO, SMTP_USER, | |
188 | + SMTP_PASS, SMTP_AUTH) | |
189 | + end | |
190 | + # メールを再構築 | |
191 | + contents = make_contents(mail) | |
192 | + # 送信 | |
193 | + sm.send_mail(contents, SMTP_FROM, ad) | |
194 | + } | |
195 | + # 後始末 | |
196 | + # 送信できた気配がなければ、そのメール情報も残しておく | |
197 | + if sm then sm.finish | |
198 | + else ng_list << MailLogInfo.new(msgid, ads.join(', '), from) | |
199 | + end | |
200 | +end | |
201 | + | |
202 | + | |
203 | +# 送信したメールのログ書き出し処理 | |
204 | +# NOTE: 送信できなかったメールも残しておく | |
205 | +def write_log(id_list, ng_list) | |
206 | + return if id_list == [] and ng_list == [] | |
207 | + | |
208 | + log = File.open(LOG_PATH, 'a') | |
209 | + ts = Time.now.strftime("%b %d %H:%M:%S") | |
210 | + id_list.each { |i| | |
211 | + log.print "#{ts} [SEND] " | |
212 | + log.print "Message-ID='#{i.msgid}', To='#{i.to}', From='#{i.from}'\n" | |
213 | + } | |
214 | + ng_list.each { |i| | |
215 | + log.print "#{ts} [REJECT] " | |
216 | + log.print "Message-ID='#{i.msgid}', To='#{i.to}', From='#{i.from}'\n" | |
217 | + } | |
218 | + log.close | |
219 | +end | |
220 | + | |
221 | + | |
222 | +################################################# | |
223 | +# main | |
224 | +################################################# | |
225 | +def main() | |
226 | + ret = main_check() | |
227 | + return ret if ret != 0 | |
228 | + | |
229 | + # 送信したメールの Message-ID, From, To が収まる | |
230 | + id_list = [] | |
231 | + # 送信できなかったメールの情報も同様に残しておく | |
232 | + ng_list = [] | |
233 | + until (m = Mail.new(STDIN)).header.empty? | |
234 | + send_mail!(m, id_list, ng_list) | |
235 | + end | |
236 | + | |
237 | + # メールのログを取っておく | |
238 | + write_log(id_list, ng_list) | |
239 | + | |
240 | + return 0 | |
241 | +end | |
242 | + | |
243 | + | |
244 | +# main の実行 | |
245 | +exit main() | |
246 | +__END__ |
@@ -0,0 +1,39 @@ | ||
1 | +使用法: simrm.rb MODE [MBOX] | |
2 | +MODE: clean, dcs, dcsm, dcsp, mbox, pop3, send, stdin | |
3 | + | |
4 | +例: ruby simrm.rb clean | |
5 | +例: ruby simrm.rb dcs | |
6 | +例: ruby simrm.rb dcsm /var/mail/user | |
7 | +例: ruby simrm.rb dcsp | |
8 | +例: ruby simrm.rb hup | |
9 | +例: ruby simrm.rb kill | |
10 | +例: ruby simrm.rb mbox /var/mail/user | |
11 | +例: ruby simrm.rb pop3 | |
12 | +例: ruby simrm.rb send | |
13 | +例: cat mailbox | ruby simrm.rb stdin | |
14 | + | |
15 | +clean -> データディレクトリに未送信のまま残ってしまったメールを送信し、 | |
16 | + その後、削除します。 | |
17 | +dcs -> デーモン化した後、clean, send を実行し続けます。 | |
18 | +dcsm -> デーモン化した後、clean, send, mbox を実行し続けます。 | |
19 | +dcsp -> デーモン化した後、clean, send, pop3 を実行し続けます。 | |
20 | +hup -> 既存 simrm のデーモンを再起動します (pid ファイル利用) | |
21 | +kill -> 既存 simrm のデーモンを kill します (pid ファイル利用) | |
22 | +mbox -> 指定のメールボックスを参照し、データディレクトリへ格納します。 | |
23 | +pop3 -> POP3 サーバに接続し、このシステム宛のメール内容を取得、 | |
24 | + その後、データディレクトリへ格納します。 | |
25 | +send -> データディレクトリに格納されているメールが指定時刻ならば送信します。 | |
26 | +stdin -> 標準入力からメール内容を取得し、データディレクトリへ格納します。 | |
27 | + | |
28 | +注意: mbox モードのときは専用のメールアドレスが必要なため、 | |
29 | + 新規にユーザを追加する、 | |
30 | + または aliases にアカウントを追加することをお勧めします。 | |
31 | + pop3 モードの時は受信メールの宛先が | |
32 | + 既存ユーザと被らないように注意してください。 | |
33 | + (メールアドレスは被りますが、名前を被らないようにしてください。 | |
34 | + 例: foo <foo@hoge.net> ではなく、SimRM <foo@hoge.net> を使う) | |
35 | + | |
36 | +注意: ファイル config.rb 内の変数 DATADIR, MY_ADDRESS, SMTP_HOST 等が | |
37 | + 正しく設定されているか確認してください。 | |
38 | + 確認及び編集をするには、エディタで config.rb を直接開いてください。 | |
39 | + |
@@ -0,0 +1,15 @@ | ||
1 | +# SimRM ユーザリストファイル | |
2 | +# | |
3 | +# 重要! | |
4 | +# ユーザリストが 0 項目ならば、全てのメールをほぼ無条件に処理します。 | |
5 | +# (lib/userlist.rb を参照のこと) | |
6 | +# スパムメールなどにも反応してしまうため、十分に注意してください。 | |
7 | +# このファイルが空で addme.txt にアドレスが有る場合は、 | |
8 | +# addme.txt に記述されているアドレスのみを処理します。 | |
9 | +# | |
10 | +# ・1 行に 1 項目ずつ書いていきます。 | |
11 | +# ・各行の 1 桁目が # ならばコメント行として扱われます。 | |
12 | +# ・@ が入っていなければ無視されます。 | |
13 | +# ・このファイルは EUC で記述してください | |
14 | +# | |
15 | + |