/app/models/mail_handler.rb

https://github.com/shereefb/redmine · Ruby · 280 lines · 208 code · 27 blank · 45 comment · 42 complexity · 15dd3ab2cb3c9d7209ee9087d38ffe42 MD5 · raw file

  1. # redMine - project management software
  2. # Copyright (C) 2006-2007 Jean-Philippe Lang
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; either version 2
  7. # of the License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  17. class MailHandler < ActionMailer::Base
  18. include ActionView::Helpers::SanitizeHelper
  19. class UnauthorizedAction < StandardError; end
  20. class MissingInformation < StandardError; end
  21. attr_reader :email, :user
  22. def self.receive(email, options={})
  23. @@handler_options = options.dup
  24. @@handler_options[:issue] ||= {}
  25. @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
  26. @@handler_options[:allow_override] ||= []
  27. # Project needs to be overridable if not specified
  28. @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
  29. # Status overridable by default
  30. @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
  31. super email
  32. end
  33. # Processes incoming emails
  34. # Returns the created object (eg. an issue, a message) or false
  35. def receive(email)
  36. @email = email
  37. @user = User.find_by_mail(email.from.to_a.first.to_s.strip)
  38. if @user && !@user.active?
  39. logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
  40. return false
  41. end
  42. if @user.nil?
  43. # Email was submitted by an unknown user
  44. case @@handler_options[:unknown_user]
  45. when 'accept'
  46. @user = User.anonymous
  47. when 'create'
  48. @user = MailHandler.create_user_from_email(email)
  49. if @user
  50. logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
  51. Mailer.deliver_account_information(@user, @user.password)
  52. else
  53. logger.error "MailHandler: could not create account for [#{email.from.first}]" if logger && logger.error
  54. return false
  55. end
  56. else
  57. # Default behaviour, emails from unknown users are ignored
  58. logger.info "MailHandler: ignoring email from unknown user [#{email.from.first}]" if logger && logger.info
  59. return false
  60. end
  61. end
  62. User.current = @user
  63. dispatch
  64. end
  65. private
  66. MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
  67. ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
  68. MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
  69. def dispatch
  70. headers = [email.in_reply_to, email.references].flatten.compact
  71. if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
  72. klass, object_id = $1, $2.to_i
  73. method_name = "receive_#{klass}_reply"
  74. if self.class.private_instance_methods.include?(method_name)
  75. send method_name, object_id
  76. else
  77. # ignoring it
  78. end
  79. elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
  80. receive_issue_reply(m[1].to_i)
  81. elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
  82. receive_message_reply(m[1].to_i)
  83. else
  84. receive_issue
  85. end
  86. rescue ActiveRecord::RecordInvalid => e
  87. # TODO: send a email to the user
  88. logger.error e.message if logger
  89. false
  90. rescue MissingInformation => e
  91. logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
  92. false
  93. rescue UnauthorizedAction => e
  94. logger.error "MailHandler: unauthorized attempt from #{user}" if logger
  95. false
  96. end
  97. # Creates a new issue
  98. def receive_issue
  99. project = target_project
  100. tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
  101. category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
  102. priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
  103. status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
  104. # check permission
  105. raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
  106. issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
  107. # check workflow
  108. if status && issue.new_statuses_allowed_to(user).include?(status)
  109. issue.status = status
  110. end
  111. issue.subject = email.subject.chomp.toutf8
  112. issue.description = plain_text_body
  113. # custom fields
  114. issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
  115. if value = get_keyword(c.name, :override => true)
  116. h[c.id] = value
  117. end
  118. h
  119. end
  120. # add To and Cc as watchers before saving so the watchers can reply to Redmine
  121. add_watchers(issue)
  122. issue.save!
  123. add_attachments(issue)
  124. logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
  125. issue
  126. end
  127. def target_project
  128. # TODO: other ways to specify project:
  129. # * parse the email To field
  130. # * specific project (eg. Setting.mail_handler_target_project)
  131. target = Project.find_by_identifier(get_keyword(:project))
  132. raise MissingInformation.new('Unable to determine target project') if target.nil?
  133. target
  134. end
  135. # Adds a note to an existing issue
  136. def receive_issue_reply(issue_id)
  137. status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
  138. issue = Issue.find_by_id(issue_id)
  139. return unless issue
  140. # check permission
  141. raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
  142. raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
  143. # add the note
  144. journal = issue.init_journal(user, plain_text_body)
  145. add_attachments(issue)
  146. # check workflow
  147. if status && issue.new_statuses_allowed_to(user).include?(status)
  148. issue.status = status
  149. end
  150. issue.save!
  151. logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
  152. journal
  153. end
  154. # Reply will be added to the issue
  155. def receive_journal_reply(journal_id)
  156. journal = Journal.find_by_id(journal_id)
  157. if journal && journal.journalized_type == 'Issue'
  158. receive_issue_reply(journal.journalized_id)
  159. end
  160. end
  161. # Receives a reply to a forum message
  162. def receive_message_reply(message_id)
  163. message = Message.find_by_id(message_id)
  164. if message
  165. message = message.root
  166. if user.allowed_to?(:add_messages, message.project) && !message.locked?
  167. reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
  168. :content => plain_text_body)
  169. reply.author = user
  170. reply.board = message.board
  171. message.children << reply
  172. add_attachments(reply)
  173. reply
  174. else
  175. raise UnauthorizedAction
  176. end
  177. end
  178. end
  179. def add_attachments(obj)
  180. if email.has_attachments?
  181. email.attachments.each do |attachment|
  182. Attachment.create(:container => obj,
  183. :file => attachment,
  184. :author => user,
  185. :content_type => attachment.content_type)
  186. end
  187. end
  188. end
  189. # Adds To and Cc as watchers of the given object if the sender has the
  190. # appropriate permission
  191. def add_watchers(obj)
  192. if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
  193. addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
  194. unless addresses.empty?
  195. watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
  196. watchers.each {|w| obj.add_watcher(w)}
  197. end
  198. end
  199. end
  200. def get_keyword(attr, options={})
  201. @keywords ||= {}
  202. if @keywords.has_key?(attr)
  203. @keywords[attr]
  204. else
  205. @keywords[attr] = begin
  206. if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
  207. $1.strip
  208. elsif !@@handler_options[:issue][attr].blank?
  209. @@handler_options[:issue][attr]
  210. end
  211. end
  212. end
  213. end
  214. # Returns the text/plain part of the email
  215. # If not found (eg. HTML-only email), returns the body with tags removed
  216. def plain_text_body
  217. return @plain_text_body unless @plain_text_body.nil?
  218. parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
  219. if parts.empty?
  220. parts << @email
  221. end
  222. plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
  223. if plain_text_part.nil?
  224. # no text/plain part found, assuming html-only email
  225. # strip html tags and remove doctype directive
  226. @plain_text_body = strip_tags(@email.body.to_s)
  227. @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
  228. else
  229. @plain_text_body = plain_text_part.body.to_s
  230. end
  231. @plain_text_body.strip!
  232. @plain_text_body
  233. end
  234. def self.full_sanitizer
  235. @full_sanitizer ||= HTML::FullSanitizer.new
  236. end
  237. # Creates a user account for the +email+ sender
  238. def self.create_user_from_email(email)
  239. addr = email.from_addrs.to_a.first
  240. if addr && !addr.spec.blank?
  241. user = User.new
  242. user.mail = addr.spec
  243. names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
  244. user.firstname = names.shift
  245. user.lastname = names.join(' ')
  246. user.lastname = '-' if user.lastname.blank?
  247. user.login = user.mail
  248. user.password = ActiveSupport::SecureRandom.hex(5)
  249. user.language = Setting.default_language
  250. user.save ? user : nil
  251. end
  252. end
  253. end