Redmineでリマインダー

社内で使ってるRedmineのチケットを定期的に状況報告をメールでするようにしたい、という話。0.8からそういう機能がつくんだけど、いまいち自分の使い方と合ってないみたいなので、こちらを参考にして自分で書いてみた。
やりたいことは次のようなこと

  • 決まった時間(朝と夕方)にプロジェクトごとにチケット情報をまとめる
    • 期限が過ぎているチケット
    • 今日が期限のチケット
    • 今日作成されたチケット
    • 今日更新されたチケット
  • プロジェクトメンバーへメール送信
  • 1メール/1プロジェクト

実際書いてみると、まだまだRedmine/RailsはおろかRubyのことも全然わかんないなー。class << selfとか何のためにやるんだろう?とか、DBからデータ取る方法が雑過ぎるような…、とかいろいろ勉強不足なのかよくわかりました。

更新: 2009/4/21

  • SMTPサーバの設定をmail.ymlから読むようにしました
  • サブミッションポートに対応(587番ポート使うやつ)

動作環境

  • 実行にはgem install tlsmailが必要です
  • 動作確認はRuby1.8.7で行っています
  • %REDMINE_HOME%\lib\hogeに適当に名前をつけて配置してください
    • %REDMINE_HOME%\libでも構いませんが、その場合は5行目の"../../config/environment"を"../config/environment"に変えてください
#!C:\Program Files\ruby-1.8\bin\ruby

ENV['RAILS_ENV'] ||= 'production'

require "rubygems"
require "tlsmail"

require File.join(File.dirname(__FILE__), "../../config/environment")
require 'time'
require 'kconv'
require 'tmail'
require 'net/smtp'

class Scheduler

  class << self
    def reminder
      statuses = IssueStatus.find(:all)
      $host = "hogehost:3000"
      
      Project.find(:all, 
                   :conditions=>["status = ?", 1],
                   :order => "name DESC").each do |project|
        
        members = project.assignable_users
        
        send_bodys = Array.new
        send_bodys << "Redmine Reminder #{Date.today} #{project.name}"
        send_bodys << <<-EOF
        <html>
          <head>
            <style>
              table.deadline{background: #cc3333}
              table.today   {background: #ffff33}
              table.created {background: #99ffcc}
              table.updated {background: #99ffff}
            </style>
          </head>
        <body>
        EOF
        send_bodys << "<h1><a href=\"http://#{$host}/projects/#{project.identifier}/issues\">#{project.name}</a> - #{Date.today}</h1>"
        send_bodys << "<p>[#{members.join(',')}]</p>"
        send_bodys << ""
        send_bodys << ""
        
        issues_by_expire = project.issues.select{|i| !i.closed?}
        issue_bodys = Array.new
        
        ###
        ### 期限に関するチケット
        ###
        deadline_issues = issues_by_expire.select {|i| i.due_date.nil? || i.overdue?}
        unless deadline_issues.empty?
          issue_bodys << "<h3>■期限切れのチケット</h3>"
          issue_bodys << %!<table class="deadline">!
          issue_bodys << "<tr><th>id</th><th>期限</th><th>担当</th><th>件名</th></tr>"
          deadline_issues.each do |i|
            assigned = members.select {|member| member.id == i.assigned_to_id}.first
            url = "http://#{$host}/issues/show/#{i.id}"
            issue_bodys << "<tr><td><a href=\"#{url}\">#{i.id}</a></td><td>#{i.due_date}</td><td>#{assigned.name if assigned}</td><td>#{i.subject}</td></tr>"
          end
          issue_bodys << "</table>"
        end
        
        today_issues = issues_by_expire.select {|i| i.due_date == Date.today}
        unless today_issues.empty?
          issue_bodys << "<h3>■期限が今日のチケット</h3>"
          issue_bodys << %!<table class="today">!
          issue_bodys << "<tr><th>id</th><th>担当</th><th>件名</th></tr>"
          today_issues.each do |i|
            assigned = members.select {|member| member.id == i.assigned_to_id}.first
            url = "http://#{$host}/issues/show/#{i.id}"
            issue_bodys << "<tr><td><a href=\"#{url}\">#{i.id}</a></td><td>#{assigned.name if assigned}</td><td>#{i.subject}</td></tr>"
          end
          issue_bodys << "</table>"
        end
        
        
        
        ###
        ### 今日更新のあったチケット
        ###
        issues_by_update = Issue.find(:all, 
                                      :conditions=>["project_id = ?", project.id])
        
        created_issues = issues_by_update.select {|i| i.created_on.year  == Time.now.year && 
                                                      i.created_on.month == Time.now.month &&
                                                      i.created_on.day   == Time.now.day}
        unless created_issues.empty?
          issue_bodys << "<h3>□今日作成されたチケット</h3>"
          issue_bodys << %!<table class="created">!
          issue_bodys << "<tr><th>id</th><th>予定時間</th><th>起票</th><th>担当</th><th>件名</th></tr>"
          created_issues.each do |i|
            assigned = members.select {|member| member.id == i.assigned_to_id}.first
            author = members.select {|member| member.id == i.author_id}.first
            url = "http://#{$host}/issues/show/#{i.id}"
            issue_bodys << "<tr><td><a href=\"#{url}\">#{i.id}</a></td><td>#{i.estimated_hours}h</td><td>#{author.name}</td><td>#{assigned.name if assigned}</td><td>#{i.subject}</td></tr>"
          end
          issue_bodys << "</table>"
        end
        
        updated_issues = issues_by_update.select {|i| i.updated_on.year  == Time.now.year && 
                                                      i.updated_on.month == Time.now.month &&
                                                      i.updated_on.day   == Time.now.day}
        unless updated_issues.empty?
          issue_bodys << "<h3>□今日更新されたチケット</h3>"
          issue_bodys << %!<table class="updated">!
          issue_bodys << "<tr><th>id</th><th>状態</th><th>進捗</th><th>担当</th><th>件名</th></tr>"
          updated_issues.each do |i|
            assigned = members.select {|member| member.id == i.assigned_to_id}.first
            status = statuses.select {|s| s.id == i.status_id}.first
            url = "http://#{$host}/issues/show/#{i.id}"
            issue_bodys << "<tr><td><a href=\"#{url}\">#{i.id}</a></td><td>#{status.name}</td><td>#{i.done_ratio}%</td><td>#{assigned.name if assigned}</td><td>#{i.subject}</td></tr>"
          end
          issue_bodys << "</table>"
        end
        
        next if issue_bodys.empty?
        
        issue_bodys << "</body></html>"
        
        #send_schedule_mail(send_bodys.concat(issue_bodys))
        send_schedule_mail(send_bodys.concat(issue_bodys), members.map(&:mail))
      end
    end

  private

  def send_schedule_mail(body, send_addr=["デフォルトのメールアドレス"])
    finddate = body.shift
    mail = TMail::Mail.new
    mail.to = *(send_addr)
    mail.from = Setting.mail_from
    conv_subject = Kconv.toutf8(finddate).split(//, 1).pack('m').chomp
    encoded_subject = "=?UTF-8?B?" + conv_subject.gsub('\n', '') + "?="
    mail.subject = encoded_subject
    mail.date = Time.now
    mail.mime_version = '1.0'

    message = TMail::Mail.new
    message.set_content_type('text', 'html', {'charset'=>'iso-2022-jp'})
    message.transfer_encoding = '7bit'
    message.body =  Kconv.tojis(body.join("\r\n"))

    mail.parts.push(message)
    mail.write_back

    filename = File.join(File.dirname(__FILE__), '../../config/email.yml')
    mailconfig = YAML::load_file(filename)['production']['smtp_settings']

    smtp_server    = mailconfig['address']
    port           = mailconfig['port']
    domain         = mailconfig['domain']
    authentication = mailconfig['authentication']
    user_name      = mailconfig['user_name']
    password       = mailconfig['password']
    
    begin
#    tls使わない場合はこっち
#    Net::SMTP.start(smtp_server) do |smtp|
#        smtp.sendmail(mail.encoded, mail.from, *(send_addr))
#    end
     Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
     Net::SMTP.start(smtp_server, port, domain, user_name, password, authentication) do |smtp|
       smtp.send_mail(mail.encoded, mail.from, *(send_addr))
     end
    rescue => ex
      puts ex.message
      if ex.class == Net::SMTPServerBusy
        sleep 3
        retry
      end
    end
    true
  end
  end
end


if __FILE__ == $0
  Scheduler.reminder
end