#!/usr/bin/env ruby
##################################################
#
#= memosend.rb
#
#  Developers:: Yasuhiro Morikawa
#  Version::    2005/04/13 17:01:11
#
#== Overview
#
#memosend はメモ書きをコマンド一つで適当なヘッダをつけて
#あるメールに送るためのスクリプトである。
#
#ドキュメントを生成したい場合は、
#rdoc <http://www.ruby-doc.org/stdlib/libdoc/rdoc/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 <http://www.ep.sci.hokudai.ac.jp/~gate>
  # によるデータベースから取得するため、このシステムがインストールされて
  # いない場合には 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 のデータベース依存である。
      # 詳しくは <http://www.ep.sci.hokudai.ac.jp/~gate/doc/gate-user-db.htm>
      # を参照せよ。
      #
      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] Memo Update"
  senditem.url     = "http://www.gfd-dennou.org/arch/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
