#!/usr/bin/env ruby
##################################################
#
#= memosend.rb
#
#Authors:: Yasuhiro MORIKAWA
#Version:: $Id: memosend.rb,v 1.3 2007/10/03 06:35:27 morikawa Exp $
#Copyright:: Copyright (C) GFD Dennou Club, 2007. All rights reserved.
#
#== SYNOPSIS
SYNOPSIS = <<-"SYNOP"
memosend はメモ書きをコマンド一つで適当なヘッダをつけて,
あるメールアドレス (デフォルトは自分) に送るためのスクリプトである.
SYNOP
#== Overview
#
#
#ドキュメントを生成したい場合は、
#rdoc
#を用いて
#
# $ rdoc memosend.rb
#
#とすること。
#
#なお、メインルーチンは一番下にあるので、送り先を変える場合などは
#そこを参照のこと。
#
#==USAGE
USAGE = "
% #{File.basename(__FILE__)} filename [options]
"
#=== Options
#
# --direct : send mail directly (default, send myself)
#
#== Known Bugs
#
#* 今のところ、特に無し
#
#== Future Plans
#
#* フッタをつけると格好良いかな?
#
#* デフォルトで起動した際は、このライブラリを require するサンプル
# スクリプトを出力できると嬉しい。
#
#* このプログラム自体はライブラリとして動くにようにし、(上記の Usage
# などには --help オプション見てね、みたいにする) Options なども
# サンプルスクリプトに書くようにするとよいなぁ。
#
#== Note
#
#* 特になし
#
#== History
#
#These entries is generated by CVS automatically.
#So don't add new information manually.
#(But please adjust old log format to latest log format manually,
#if format gap between them causes).
#
#$Log: memosend.rb,v $
#Revision 1.3 2007/10/03 06:35:27 morikawa
#* メールのサブジェクトの変更.
#* メール本文の変更.
#
#Revision 1.2 2007/10/03 06:30:25 morikawa
#* ヘルプメッセージの追加.
#* メールのサブジェクトの変更.
#* メール本文の変更.
#
#
#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/library/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__
if ARGV.size < 1
print <<-"EOF"
#{File.basename(__FILE__)}:
#{SYNOPSIS}
USAGE: #{USAGE}
EOF
exit 1
end
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 = "dcmodel ミーティングログ"
senditem.url = "http://www.gfd-dennou.org/library/dcmodel/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
dcmodel 関係で本日行ったミーティングの内容を送ります.
EOM
# senditem.message = <<-EOM
#更新情報を通知いたします。
# EOM
if $OPT_direct
senditem.to_addr_clear("dcmodel@gfd-dennou.org")
end
senditem.send
end