• R/O
  • SSH
  • HTTPS

simrm: コミット


コミットメタ情報

リビジョン7 (tree)
日時2007-12-06 19:04:06
作者isr

ログメッセージ

version 2.0 パッケージとして tag 化.

変更サマリ

差分

--- tags/2.0/mail/help.txt (nonexistent)
+++ tags/2.0/mail/help.txt (revision 7)
@@ -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+以上です。
--- tags/2.0/mail/maintenance.txt (nonexistent)
+++ tags/2.0/mail/maintenance.txt (revision 7)
@@ -0,0 +1,6 @@
1+--
2+
3+システムがメンテナンス中のため、
4+メールの受付を停止しております。
5+
6+以上です。
--- tags/2.0/mail/error.txt (nonexistent)
+++ tags/2.0/mail/error.txt (revision 7)
@@ -0,0 +1,8 @@
1+このシステムのヘルプを参照するには、件名に help と記述したメールを、
2+システムのメールアドレスに向けて送信してください。
3+なお、このメールの返信先 (Reply-To) は管理者のメールアドレスであるため、
4+間違った宛先に送らないようにしてください。
5+
6+また、どうしてもエラーが取れない場合は管理者に連絡を取ってみてください。
7+
8+以上です。
--- tags/2.0/note/plugin.txt (nonexistent)
+++ tags/2.0/note/plugin.txt (revision 7)
@@ -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 ページに更新があったらメールを送る
--- tags/2.0/INSTALL (nonexistent)
+++ tags/2.0/INSTALL (revision 7)
@@ -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]
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/lib/errmsg.rb (nonexistent)
+++ tags/2.0/lib/errmsg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/lib/utils.rb (nonexistent)
+++ tags/2.0/lib/utils.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/lib/mailext.rb (nonexistent)
+++ tags/2.0/lib/mailext.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/lib/sending.rb (nonexistent)
+++ tags/2.0/lib/sending.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/lib/userlist.rb (nonexistent)
+++ tags/2.0/lib/userlist.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/lib/pmanager.rb (nonexistent)
+++ tags/2.0/lib/pmanager.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/config.rb (nonexistent)
+++ tags/2.0/config.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/simrm.rb (nonexistent)
+++ tags/2.0/simrm.rb (revision 7)
@@ -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__
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/pukiwiki/simrm.inc.php (nonexistent)
+++ tags/2.0/pukiwiki/simrm.inc.php (revision 7)
@@ -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+?>
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/pukiwiki/mimeDecode.php (nonexistent)
+++ tags/2.0/pukiwiki/mimeDecode.php (revision 7)
@@ -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("ナ&#8216;"=>"テオ","ナア"=>"テサ","ナ?=>"テ&#8226;","ナー"
73+ =>"テ&#8250;");
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+?>
--- tags/2.0/plugin/plugin.rb (nonexistent)
+++ tags/2.0/plugin/plugin.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/plugin_utils.rb (nonexistent)
+++ tags/2.0/plugin/plugin_utils.rb (revision 7)
@@ -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
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/ls.smplg.rb (nonexistent)
+++ tags/2.0/plugin/ls.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/sample.smplg.rb (nonexistent)
+++ tags/2.0/plugin/sample.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/fortune.smplg.rb (nonexistent)
+++ tags/2.0/plugin/fortune.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/status.smplg.rb (nonexistent)
+++ tags/2.0/plugin/status.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/md5.smplg.rb (nonexistent)
+++ tags/2.0/plugin/md5.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/ifcat.smplg.rb (nonexistent)
+++ tags/2.0/plugin/ifcat.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/reload.smplg.rb (nonexistent)
+++ tags/2.0/plugin/reload.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/cat.smplg.rb (nonexistent)
+++ tags/2.0/plugin/cat.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/README.txt (nonexistent)
+++ tags/2.0/plugin/README.txt (revision 7)
@@ -0,0 +1,4 @@
1+プラグインを作成するときは、make_plugin.rb を実行することで
2+ベースファイルが作成できます。plugin.rb と sample.smplg.rb を
3+参考にしてベースファイルを編集することで、少しは楽に作成でき
4+ると思います。
--- tags/2.0/plugin/help.smplg.rb (nonexistent)
+++ tags/2.0/plugin/help.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/make_plugin.rb (nonexistent)
+++ tags/2.0/plugin/make_plugin.rb (revision 7)
@@ -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!"
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/plugin/list.smplg.rb (nonexistent)
+++ tags/2.0/plugin/list.smplg.rb (revision 7)
@@ -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__
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/LICENSE (nonexistent)
+++ tags/2.0/LICENSE (revision 7)
@@ -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+
--- tags/2.0/smisp/smisp.rb (nonexistent)
+++ tags/2.0/smisp/smisp.rb (revision 7)
@@ -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__
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Id
\ No newline at end of property
--- tags/2.0/usage.txt (nonexistent)
+++ tags/2.0/usage.txt (revision 7)
@@ -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+
--- tags/2.0/template/userlist.txt (nonexistent)
+++ tags/2.0/template/userlist.txt (revision 7)
@@ -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+
旧リポジトリブラウザで表示