#!/usr/bin/env ruby ################################################## # #= memosend.rb # # Developers:: Yasuhiro Morikawa # Version:: 2005/04/13 17:01:11 # #== Overview # #memosend はメモ書きをコマンド一つで適当なヘッダをつけて #あるメールに送るためのスクリプトである。 # #ドキュメントを生成したい場合は、 #rdoc #を用いて # # $ rdoc memosend.rb # #とすること。 # #なお、メインルーチンは一番下にあるので、送り先を変える場合などは #そこを参照のこと。 # #== Usage # # % memosend.rb filename [options] # # #=== Options # # --direct : send mail directly (default, send myself) # #== Known Bugs # #* 今のところ、特に無し # #== Future Plans # #* フッタをつけると格好良いかな? # #* デフォルトで起動した際は、このライブラリを require するサンプル # スクリプトを出力できると嬉しい。 # #* このプログラム自体はライブラリとして動くにようにし、(上記の Usage # などには --help オプション見てね、みたいにする) Options なども # サンプルスクリプトに書くようにするとよいなぁ。 # #== Note # #* 特になし # #== History # #* 2005/04/13 17:01:11 # * 全体的に作り換え。クラスとしての体系を整える。 # アドレス変換や、拡張子変換をメソッドから指定できるようにした。 # また、サブジェクトや URL もメソッドから指定できるようにした。 # #* 2005/04/12 15:53:41 # * 送り先と、ヘッダにつけるメッセージを追加。 # #* 2005/04/06 17:30:59 # * メッセージの更新。yyhlab に送らないようにする。 # #* 2005/03/31 18:01:21 # * いろいろダサいけど、とりあえずできた。 # #* 2005/03/31 15:27:23 # * 作ってみた # ################################################## require "getoptlong" # for option_parse require "etc" # ユーザ ID 解析 require "date" # 日付解析 require "kconv" # 文字コード取扱 require "net/smtp" # メール送信機能 ###################################################################### class MemoSend attr_reader :memofile, :gate_user_show, :uid, :domain, :toaddr attr_reader :replaced_from attr_accessor :smtp_server, :smtp_port, :user_name, :user_jpname attr_accessor :from, :addr_replace_hash, :permit_from_group attr_accessor :subject, :message, :url, :ext_replace_hash # # memofile にメモ書きファイルを指定する。 # もしもファイルが存在しない場合や読み取れない場合は終了。 # def initialize(memofile) @memofile = memofile # メモ書きファイル # memofile に関するエラー処理 if !(memofile.instance_of?(String)) then raise ArgumentError, "Error: file is not specified. \n" elsif !(File.exist?(memofile)) then raise ArgumentError, "Error: \"#{memofile}\" is not found. \n" elsif !(File.readable?(memofile)) then raise ArgumentError, "Error: \"#{memofile}\" is not readable. \n" end # # 各種の送信の設定値 # @smtp_server = "localhost" # SMTP サーバ @smtp_port = 25 # SMTP ポート @domain = "gfd-dennou.org" # メールの送付元ドメイン @gate_user_show = "/usr/local/bin/gate-user-show" # gate コマンドのパス debug(@smtp_server, @smtp_port, @domain) debug(@gate_user_show) @uid = Process.uid # UID @user_name = username_from_uid(@uid) # ユーザ名 @user_jpname = jpname_from_uid(@uid, true) || @user_name # ユーザ(日本語) debug(@uid, @user_name, @user_jpname) @from = @user_name + "@" + @domain # 送信元メールアドレス @replaced_from = "#{@from}" # 置き換え用 from @toaddr = [@replaced_from] # 送信先メールアドレス @addr_replace_hash = Hash.new # アドレス変換エントリ @permit_from_group = false # グループからの投稿許可 # # メール本文に関するの設定値 # @subject = "[MemoSend] Test Post" # Subject のヘッダ @message = "This is Test mail by memosend.rb" # メッセージ @url = "http://www.gfd-dennou.org/arch/dcmodel/bin" # URL @ext_replace_hash = Hash.new # ファイル名拡張子変換エントリ end # # デバッグ出力用メソッド。組み込み関数 $DEBUG が真の場合 (つまり、 # プログラムを $ ruby -d ./xxxxxx.rb として呼び出した場合) に # debug メソッドに代入された変数を出力する。 # def debug(*args) p [caller.first, *args] if $DEBUG end private :debug # # uid を明示的に指定する。それにより、user_name と user_jpname 、 # および from が自動的に設定される。無効な UID を指定した場合には # エラーを返す。 # def set_uid(uid=nil) return nil unless uid @uid = uid @user_name = username_from_uid(@uid) # ユーザ名 @user_jpname = jpname_from_uid(@uid, true) || @user_name # ユーザ名 (日本語) @from = @user_name + "@" + @domain # 送信元メールアドレス debug(@uid, @user_name, @user_jpname, @from) return true end # # ドメインを明示的に指定する。それにより、from も # 自動的に設定される。 # def set_domain(domain=nil) return nil unless str_and_notspace?(domain) @domain = domain @from = @user_name + "@" + @domain # 送信元メールアドレス return true end # # gate_user_show コマンドを入れ替え、user_jpname を再取得する。 # command には gate-user-show コマンドのパスを設定する。 # プロセスの uid を用いたくない場合には uid に値を設定する。 # def gate_user_show_replace(command=nil, uid=nil) return nil unless str_and_notspace?(command) @gate_user_show = command @user_jpname = jpname_from_uid(nil, true) || @user_name return true end # # アドレスを追加する。 # def to_addr(*addrs) addrs.flatten! # 配列の平滑化 (1次元配列化) addrs.delete_if{|dir| !dir.instance_of?(String)} # 文字列でないものは削除 addrs.collect!{|dir| dir = dir.strip} # 前後の空白を除く addrs.uniq! # 重複を無くす @toaddr.push(addrs) @toaddr.flatten! # 配列の平滑化 (1次元配列化) @toaddr.delete_if{|dir| !dir.instance_of?(String)} # 文字列でないものは削除 @toaddr.collect!{|dir| dir = dir.strip} # 前後の空白を除く @toaddr.uniq! # 重複を無くす debug(@toaddr) end # # 今までのアドレスをクリアし、新たにアドレスを設定する。 # def to_addr_clear(*addrs) addrs.flatten! # 配列の平滑化 (1次元配列化) addrs.delete_if{|dir| !dir.instance_of?(String)} # 文字列でないものは削除 addrs.collect!{|dir| dir = dir.strip} # 前後の空白を除く addrs.uniq! # 重複を無くす if (addrs.size > 0) @toaddr = Array.new @toaddr.push(addrs) @toaddr.flatten! # 配列の平滑化 (1次元配列化) @toaddr.delete_if{|dir| !dir.instance_of?(String)} # 文字列でないものは削除 @toaddr.collect!{|dir| dir = dir.strip} # 前後の空白を除く @toaddr.uniq! # 重複を無くす end debug(@toaddr) end attr_reader :mail_src # # メールを送信する。この際、以下のチェックを行い、適合しない場合 # エラーを返して終了する。 # # * user_name や user_jpname が文字である。 # * user_name が group ユーザで無い場合。 # * ただし、permit_from_group を true にしている場合は例外。 # * toaddr が空の配列でなく、且つ中に空白ではない文字列があること。 # def send # # チェック項目 # # * user_name や user_jpname が文字である事をチェック if !(str_and_notspace?(@user_name)) then raise ArgumentError, "Error: user_name is invalid. \n" elsif !(str_and_notspace?(@user_jpname)) then raise ArgumentError, "Error: user_jpname is invalid. \n" end # * user_name が group ユーザで無いことをチェック if !@permit_from_group && check_group_user(@uid) then error_msg = "Error: Now your UID is group user.\n" error_msg << " Please exit and retry, \n" error_msg << " or set \"permit_from_group = true\".\n" error_msg << " (In this case, mail is sent to group by default.)\n" raise ArgumentError, error_msg end # * toaddr が空の配列でなく、且つ中に空白ではない文字列があること。 if !array_and_notzero?(@toaddr) then raise ArgumentError, "Error : Invalid addresses.\n" end @toaddr.each{|to| if !str_and_notspace?(to) then raise ArgumentError, "Error : Invalid addresses.\n" end } # # アドレスの置換 # @replaced_from = addr_replace(@from) debug(@replaced_from) # # メール本文の作成 # @mail_src = make_mail_entire Net::SMTP.start( @smtp_server, @smtp_port ) {|smtp| @toaddr.each{|to| debug(@replaced_from, to) smtp.send_mail(@mail_src, @replaced_from, to) } } print "mail send successfull. (from #{@replaced_from})\n" @toaddr.each{|addr| print " to #{addr} \n" } end # # 引数 uid に対応するユーザ名がグループユーザ (その GID に他のユーザ # を含むユーザ) であるかどうかを確かめるメソッド。 # uid に nil を入れる場合にはプロセスの UID のユーザを探査する。 # # グループユーザである場合は true を、そうでない場合は false を返す。 # def check_group_user(uid=nil) unless uid current_uid = Process.uid else current_uid = uid end memberlist = Etc.getgrgid(uid).mem if array_and_notzero?(memberlist) then return true else return false end end private :check_group_user # # 引数 uid に対応するユーザ名 (ログイン名) を返す。 # uid に nil を与えた場合はプロセスの uid に対応するユーザ名 (ログイン名) # を返す。uid が無効なものである場合、エラーを返す。 # def username_from_uid(uid=nil) unless uid pw = Etc.getpwuid(Process.uid) or return nil else pw = Etc.getpwuid(uid) or return nil end user_name = pw.name return user_name end private :username_from_uid # # 引数 uid に対応するユーザ名 (日本語) を返す。 # uid に nil を与えた場合はプロセスの uid に対応するユーザ名 (日本語) # を返す。 # # 日本語名は gate-toroku-system # によるデータベースから取得するため、このシステムがインストールされて # いない場合には nil を返す。 # # 引数 family_name に true を与えた場合、姓のみを返そうと試みる。 # データベースの和名が半角空白または全角空白で区切られる場合、 # 姓のみを返すことが可能である。 # def jpname_from_uid(uid=nil, family_name=true) if FileTest.executable?(@gate_user_show) gate_user_database = IO.popen("#{@gate_user_show} #{@user_name}") # # 以下は、完全に gate-toroku-system のデータベース依存である。 # 詳しくは # を参照せよ。 # while gate_user_data = gate_user_database.gets do gate_user_data.chomp! if /^kname/ =~ gate_user_data jpname_key, jpname_value = gate_user_data.split(/: /, 2) Kconv::toeuc(jpname_value) end # 名字だけ取り出そうと試みる。 # (姓名の間に半角空白または全角空白が無い時は無理) if family_name && /(.+)[\s| ]+.+/e =~ jpname_value then jpname = $1 else jpname = jpname_value end end else jpname = nil end return jpname end private :jpname_from_uid # # mew の形式の Date ヘッダにつける文字を返す # def date_mewform date_mewform = Time.new return date_mewform.strftime("%a, %d %b %Y %H:%M:%S %z (%Z)") end private :date_mewform # # 拡張子の変換を行うメソッド。filename に与えられた文字列の内、 # ハッシュ ext_replace_hash のキーにヒットする拡張子を値のもの # に入れ替える。 # def ext_replace(filename) return nil unless str_and_notspace?(filename) aftername = filename @ext_replace_hash.each{|key, value| if /^(.*)\.#{key}$/ =~ filename then aftername = $1 + ".#{value}" end } return aftername end private :ext_replace # # アドレスの変換を行うメソッド。addr に与えられた文字列の内、 # "@" よりも前のユーザ名に相当する部分が ハッシュ addr_replace_hash # のキーにヒットするものを値に入れ替える。include_domain を # true にすると、ドメインまで一致しないと置換されない。 # def addr_replace(addr=nil, include_domain=nil) return nil unless str_and_notspace?(addr) debug(addr, include_domain) replaced = addr if !include_domain then check_addr = addr.split(/@/)[0] else check_addr = addr end debug(@addr_replace_hash) @addr_replace_hash.each{|key, value| if /^#{key}@*.*$/ =~ check_addr.chomp.strip then replaced = value else replaced = addr end } debug(replaced) return replaced end private :addr_replace # # 代入された変数が、文字列で、且つ空白文字のみではないことを # 調べるメソッド。日本語であっても、文字列が入っていれば true を返す。 # def str_and_notspace?(obj) debug(obj) if !obj.instance_of?(String) then return false end # 日本語の文字列も対応できるように Kconv::toeuc(obj) if /\w+/e =~ obj.chomp.strip then return true else return false end end private :str_and_notspace? # # 代入された変数が、配列で、且つゼロ配列ではないことを # 調べるメソッド # def array_and_notzero?(obj) debug(obj) if obj.instance_of?(Array) && obj.size > 0 then return true else return false end end private :array_and_notzero? # # メールの本文を作成して返す # def make_mail_entire # 複数の to を文字に mail_head_toaddr = "To: " tofirst = true @toaddr.each{|to| mail_head_toaddr << "," unless tofirst mail_head_toaddr << " #{to}" tofirst = false } mail_head_toaddr << "\n" # # ヘッダ作成 # mail_head = "" mail_head << "Subject: #{@subject} (#{Date::today})\n" mail_head << "From: #{@replaced_from}\n" mail_head << mail_head_toaddr mail_head << "Date: #{date_mewform}\n" mail_head << "X-Sender: "+ "memosend.rb\n" mail_head << "Content-Type: Text/Plain; charset=iso-2022-jp\n" mail_head << "Content-Transfer-Encoding: 7bit\n" debug(mail_head) # # BODY イントロ # mail_body_intro = "" # ヘッダとの間の空白 (重要) mail_body_intro = <<-EndOfIntro #{@user_jpname}です #{@message} URL: #{@url}/#{ext_replace(@memofile)} EndOfIntro # # BODY 全体の作成 # mail_body = "" mail_body = open("#{@memofile}"){|io| io.read} mail_entire = mail_head + "\n" + mail_body_intro + "\n" + mail_body return Kconv::tojis(mail_entire) end private :make_mail_entire end ############################################################################ ################################################## ## +++ Main Routine +++ ## ## parse options parser = GetoptLong.new parser.set_options( ### global option ### # for direct (send directly) ['--direct', GetoptLong::NO_ARGUMENT] ) begin parser.each_option do |name, arg| eval "$OPT_#{name.sub(/^--/, '').gsub(/-/, '_')} = '#{arg}'" # strage option value to $OPT_val end rescue exit(1) end if $0 == __FILE__ senditem = MemoSend.new(ARGV.shift) # senditem.smtp_server = "mail.ep.sci.hokudai.ac.jp" # senditem.smtp_post = 25 # senditem.set_uid(500) # senditem.set_domain("ep.sci.hokudai.ac.jp") # senditem.gate_user_show_replace("gate-user-show") # senditem.to_addr("morikawa@ep.sci.hokudai.ac.jp") # senditem.user_name = "morikawa" # senditem.user_jpname = "森川 靖大" # senditem.from = "morikawa@ep.sci.hokudai.ac.jp" senditem.addr_replace_hash.store("morikawa", "morikawa@ep.sci.hokudai.ac.jp") senditem.addr_replace_hash.store("momoko", "momoko@ees.hokudai.ac.jp") # senditem.permit_from_group = true senditem.subject = "[DCPAM] meeting log" senditem.url = "http://www.gfd-dennou.org/library/dcpam/memo" # senditem.ext_replace_hash.store("txt", "TXT") # senditem.ext_replace_hash.store("rb", "hoge") senditem.ext_replace_hash.store("rd", "htm") senditem.message = <<-EOM 本日行った DCPAM に関する打ち合わせの内容です. EOM # senditem.message = <<-EOM #更新情報を通知いたします。 # EOM if $OPT_direct senditem.to_addr_clear("dcmodel@gfd-dennou.org") end senditem.send end