##// END OF EJS Templates
Don't abort processing of emails when an email triggers a parsing exception (#16122)....
Jean-Philippe Lang -
r12720:ddcad64aa28e
parent child
Show More
@@ -1,541 +1,549
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 if @@handler_options[:allow_override].is_a?(String)
32 if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 end
34 end
35 @@handler_options[:allow_override] ||= []
35 @@handler_options[:allow_override] ||= []
36 # Project needs to be overridable if not specified
36 # Project needs to be overridable if not specified
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 # Status overridable by default
38 # Status overridable by default
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40
40
41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
44
44
45 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
45 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
46 super(email)
46 super(email)
47 end
47 end
48
48
49 # Receives an email and rescues any exception
50 def self.safe_receive(*args)
51 receive(*args)
52 rescue => e
53 logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger
54 return false
55 end
56
49 # Extracts MailHandler options from environment variables
57 # Extracts MailHandler options from environment variables
50 # Use when receiving emails with rake tasks
58 # Use when receiving emails with rake tasks
51 def self.extract_options_from_env(env)
59 def self.extract_options_from_env(env)
52 options = {:issue => {}}
60 options = {:issue => {}}
53 %w(project status tracker category priority).each do |option|
61 %w(project status tracker category priority).each do |option|
54 options[:issue][option.to_sym] = env[option] if env[option]
62 options[:issue][option.to_sym] = env[option] if env[option]
55 end
63 end
56 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
64 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
57 options[option.to_sym] = env[option] if env[option]
65 options[option.to_sym] = env[option] if env[option]
58 end
66 end
59 options
67 options
60 end
68 end
61
69
62 def logger
70 def logger
63 Rails.logger
71 Rails.logger
64 end
72 end
65
73
66 cattr_accessor :ignored_emails_headers
74 cattr_accessor :ignored_emails_headers
67 @@ignored_emails_headers = {
75 @@ignored_emails_headers = {
68 'X-Auto-Response-Suppress' => 'oof',
76 'X-Auto-Response-Suppress' => 'oof',
69 'Auto-Submitted' => /^auto-/
77 'Auto-Submitted' => /^auto-/
70 }
78 }
71
79
72 # Processes incoming emails
80 # Processes incoming emails
73 # Returns the created object (eg. an issue, a message) or false
81 # Returns the created object (eg. an issue, a message) or false
74 def receive(email)
82 def receive(email)
75 @email = email
83 @email = email
76 sender_email = email.from.to_a.first.to_s.strip
84 sender_email = email.from.to_a.first.to_s.strip
77 # Ignore emails received from the application emission address to avoid hell cycles
85 # Ignore emails received from the application emission address to avoid hell cycles
78 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
86 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
79 if logger
87 if logger
80 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
88 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
81 end
89 end
82 return false
90 return false
83 end
91 end
84 # Ignore auto generated emails
92 # Ignore auto generated emails
85 self.class.ignored_emails_headers.each do |key, ignored_value|
93 self.class.ignored_emails_headers.each do |key, ignored_value|
86 value = email.header[key]
94 value = email.header[key]
87 if value
95 if value
88 value = value.to_s.downcase
96 value = value.to_s.downcase
89 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
97 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
90 if logger
98 if logger
91 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
99 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
92 end
100 end
93 return false
101 return false
94 end
102 end
95 end
103 end
96 end
104 end
97 @user = User.find_by_mail(sender_email) if sender_email.present?
105 @user = User.find_by_mail(sender_email) if sender_email.present?
98 if @user && !@user.active?
106 if @user && !@user.active?
99 if logger
107 if logger
100 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
108 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
101 end
109 end
102 return false
110 return false
103 end
111 end
104 if @user.nil?
112 if @user.nil?
105 # Email was submitted by an unknown user
113 # Email was submitted by an unknown user
106 case @@handler_options[:unknown_user]
114 case @@handler_options[:unknown_user]
107 when 'accept'
115 when 'accept'
108 @user = User.anonymous
116 @user = User.anonymous
109 when 'create'
117 when 'create'
110 @user = create_user_from_email
118 @user = create_user_from_email
111 if @user
119 if @user
112 if logger
120 if logger
113 logger.info "MailHandler: [#{@user.login}] account created"
121 logger.info "MailHandler: [#{@user.login}] account created"
114 end
122 end
115 add_user_to_group(@@handler_options[:default_group])
123 add_user_to_group(@@handler_options[:default_group])
116 unless @@handler_options[:no_account_notice]
124 unless @@handler_options[:no_account_notice]
117 Mailer.account_information(@user, @user.password).deliver
125 Mailer.account_information(@user, @user.password).deliver
118 end
126 end
119 else
127 else
120 if logger
128 if logger
121 logger.error "MailHandler: could not create account for [#{sender_email}]"
129 logger.error "MailHandler: could not create account for [#{sender_email}]"
122 end
130 end
123 return false
131 return false
124 end
132 end
125 else
133 else
126 # Default behaviour, emails from unknown users are ignored
134 # Default behaviour, emails from unknown users are ignored
127 if logger
135 if logger
128 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
136 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
129 end
137 end
130 return false
138 return false
131 end
139 end
132 end
140 end
133 User.current = @user
141 User.current = @user
134 dispatch
142 dispatch
135 end
143 end
136
144
137 private
145 private
138
146
139 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
147 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
140 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
148 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
141 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
149 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
142
150
143 def dispatch
151 def dispatch
144 headers = [email.in_reply_to, email.references].flatten.compact
152 headers = [email.in_reply_to, email.references].flatten.compact
145 subject = email.subject.to_s
153 subject = email.subject.to_s
146 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
154 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
147 klass, object_id = $1, $2.to_i
155 klass, object_id = $1, $2.to_i
148 method_name = "receive_#{klass}_reply"
156 method_name = "receive_#{klass}_reply"
149 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
157 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
150 send method_name, object_id
158 send method_name, object_id
151 else
159 else
152 # ignoring it
160 # ignoring it
153 end
161 end
154 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
162 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
155 receive_issue_reply(m[1].to_i)
163 receive_issue_reply(m[1].to_i)
156 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
164 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
157 receive_message_reply(m[1].to_i)
165 receive_message_reply(m[1].to_i)
158 else
166 else
159 dispatch_to_default
167 dispatch_to_default
160 end
168 end
161 rescue ActiveRecord::RecordInvalid => e
169 rescue ActiveRecord::RecordInvalid => e
162 # TODO: send a email to the user
170 # TODO: send a email to the user
163 logger.error e.message if logger
171 logger.error e.message if logger
164 false
172 false
165 rescue MissingInformation => e
173 rescue MissingInformation => e
166 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
174 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
167 false
175 false
168 rescue UnauthorizedAction => e
176 rescue UnauthorizedAction => e
169 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
177 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
170 false
178 false
171 end
179 end
172
180
173 def dispatch_to_default
181 def dispatch_to_default
174 receive_issue
182 receive_issue
175 end
183 end
176
184
177 # Creates a new issue
185 # Creates a new issue
178 def receive_issue
186 def receive_issue
179 project = target_project
187 project = target_project
180 # check permission
188 # check permission
181 unless @@handler_options[:no_permission_check]
189 unless @@handler_options[:no_permission_check]
182 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
190 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
183 end
191 end
184
192
185 issue = Issue.new(:author => user, :project => project)
193 issue = Issue.new(:author => user, :project => project)
186 issue.safe_attributes = issue_attributes_from_keywords(issue)
194 issue.safe_attributes = issue_attributes_from_keywords(issue)
187 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
195 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
188 issue.subject = cleaned_up_subject
196 issue.subject = cleaned_up_subject
189 if issue.subject.blank?
197 if issue.subject.blank?
190 issue.subject = '(no subject)'
198 issue.subject = '(no subject)'
191 end
199 end
192 issue.description = cleaned_up_text_body
200 issue.description = cleaned_up_text_body
193
201
194 # add To and Cc as watchers before saving so the watchers can reply to Redmine
202 # add To and Cc as watchers before saving so the watchers can reply to Redmine
195 add_watchers(issue)
203 add_watchers(issue)
196 issue.save!
204 issue.save!
197 add_attachments(issue)
205 add_attachments(issue)
198 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
206 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
199 issue
207 issue
200 end
208 end
201
209
202 # Adds a note to an existing issue
210 # Adds a note to an existing issue
203 def receive_issue_reply(issue_id, from_journal=nil)
211 def receive_issue_reply(issue_id, from_journal=nil)
204 issue = Issue.find_by_id(issue_id)
212 issue = Issue.find_by_id(issue_id)
205 return unless issue
213 return unless issue
206 # check permission
214 # check permission
207 unless @@handler_options[:no_permission_check]
215 unless @@handler_options[:no_permission_check]
208 unless user.allowed_to?(:add_issue_notes, issue.project) ||
216 unless user.allowed_to?(:add_issue_notes, issue.project) ||
209 user.allowed_to?(:edit_issues, issue.project)
217 user.allowed_to?(:edit_issues, issue.project)
210 raise UnauthorizedAction
218 raise UnauthorizedAction
211 end
219 end
212 end
220 end
213
221
214 # ignore CLI-supplied defaults for new issues
222 # ignore CLI-supplied defaults for new issues
215 @@handler_options[:issue].clear
223 @@handler_options[:issue].clear
216
224
217 journal = issue.init_journal(user)
225 journal = issue.init_journal(user)
218 if from_journal && from_journal.private_notes?
226 if from_journal && from_journal.private_notes?
219 # If the received email was a reply to a private note, make the added note private
227 # If the received email was a reply to a private note, make the added note private
220 issue.private_notes = true
228 issue.private_notes = true
221 end
229 end
222 issue.safe_attributes = issue_attributes_from_keywords(issue)
230 issue.safe_attributes = issue_attributes_from_keywords(issue)
223 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
231 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
224 journal.notes = cleaned_up_text_body
232 journal.notes = cleaned_up_text_body
225 add_attachments(issue)
233 add_attachments(issue)
226 issue.save!
234 issue.save!
227 if logger
235 if logger
228 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
236 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
229 end
237 end
230 journal
238 journal
231 end
239 end
232
240
233 # Reply will be added to the issue
241 # Reply will be added to the issue
234 def receive_journal_reply(journal_id)
242 def receive_journal_reply(journal_id)
235 journal = Journal.find_by_id(journal_id)
243 journal = Journal.find_by_id(journal_id)
236 if journal && journal.journalized_type == 'Issue'
244 if journal && journal.journalized_type == 'Issue'
237 receive_issue_reply(journal.journalized_id, journal)
245 receive_issue_reply(journal.journalized_id, journal)
238 end
246 end
239 end
247 end
240
248
241 # Receives a reply to a forum message
249 # Receives a reply to a forum message
242 def receive_message_reply(message_id)
250 def receive_message_reply(message_id)
243 message = Message.find_by_id(message_id)
251 message = Message.find_by_id(message_id)
244 if message
252 if message
245 message = message.root
253 message = message.root
246
254
247 unless @@handler_options[:no_permission_check]
255 unless @@handler_options[:no_permission_check]
248 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
256 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
249 end
257 end
250
258
251 if !message.locked?
259 if !message.locked?
252 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
260 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
253 :content => cleaned_up_text_body)
261 :content => cleaned_up_text_body)
254 reply.author = user
262 reply.author = user
255 reply.board = message.board
263 reply.board = message.board
256 message.children << reply
264 message.children << reply
257 add_attachments(reply)
265 add_attachments(reply)
258 reply
266 reply
259 else
267 else
260 if logger
268 if logger
261 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
269 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
262 end
270 end
263 end
271 end
264 end
272 end
265 end
273 end
266
274
267 def add_attachments(obj)
275 def add_attachments(obj)
268 if email.attachments && email.attachments.any?
276 if email.attachments && email.attachments.any?
269 email.attachments.each do |attachment|
277 email.attachments.each do |attachment|
270 next unless accept_attachment?(attachment)
278 next unless accept_attachment?(attachment)
271 obj.attachments << Attachment.create(:container => obj,
279 obj.attachments << Attachment.create(:container => obj,
272 :file => attachment.decoded,
280 :file => attachment.decoded,
273 :filename => attachment.filename,
281 :filename => attachment.filename,
274 :author => user,
282 :author => user,
275 :content_type => attachment.mime_type)
283 :content_type => attachment.mime_type)
276 end
284 end
277 end
285 end
278 end
286 end
279
287
280 # Returns false if the +attachment+ of the incoming email should be ignored
288 # Returns false if the +attachment+ of the incoming email should be ignored
281 def accept_attachment?(attachment)
289 def accept_attachment?(attachment)
282 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
290 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
283 @excluded.each do |pattern|
291 @excluded.each do |pattern|
284 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
292 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
285 if attachment.filename.to_s =~ regexp
293 if attachment.filename.to_s =~ regexp
286 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
294 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
287 return false
295 return false
288 end
296 end
289 end
297 end
290 true
298 true
291 end
299 end
292
300
293 # Adds To and Cc as watchers of the given object if the sender has the
301 # Adds To and Cc as watchers of the given object if the sender has the
294 # appropriate permission
302 # appropriate permission
295 def add_watchers(obj)
303 def add_watchers(obj)
296 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
304 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
297 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
305 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
298 unless addresses.empty?
306 unless addresses.empty?
299 User.active.where('LOWER(mail) IN (?)', addresses).each do |w|
307 User.active.where('LOWER(mail) IN (?)', addresses).each do |w|
300 obj.add_watcher(w)
308 obj.add_watcher(w)
301 end
309 end
302 end
310 end
303 end
311 end
304 end
312 end
305
313
306 def get_keyword(attr, options={})
314 def get_keyword(attr, options={})
307 @keywords ||= {}
315 @keywords ||= {}
308 if @keywords.has_key?(attr)
316 if @keywords.has_key?(attr)
309 @keywords[attr]
317 @keywords[attr]
310 else
318 else
311 @keywords[attr] = begin
319 @keywords[attr] = begin
312 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
320 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
313 (v = extract_keyword!(plain_text_body, attr, options[:format]))
321 (v = extract_keyword!(plain_text_body, attr, options[:format]))
314 v
322 v
315 elsif !@@handler_options[:issue][attr].blank?
323 elsif !@@handler_options[:issue][attr].blank?
316 @@handler_options[:issue][attr]
324 @@handler_options[:issue][attr]
317 end
325 end
318 end
326 end
319 end
327 end
320 end
328 end
321
329
322 # Destructively extracts the value for +attr+ in +text+
330 # Destructively extracts the value for +attr+ in +text+
323 # Returns nil if no matching keyword found
331 # Returns nil if no matching keyword found
324 def extract_keyword!(text, attr, format=nil)
332 def extract_keyword!(text, attr, format=nil)
325 keys = [attr.to_s.humanize]
333 keys = [attr.to_s.humanize]
326 if attr.is_a?(Symbol)
334 if attr.is_a?(Symbol)
327 if user && user.language.present?
335 if user && user.language.present?
328 keys << l("field_#{attr}", :default => '', :locale => user.language)
336 keys << l("field_#{attr}", :default => '', :locale => user.language)
329 end
337 end
330 if Setting.default_language.present?
338 if Setting.default_language.present?
331 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
339 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
332 end
340 end
333 end
341 end
334 keys.reject! {|k| k.blank?}
342 keys.reject! {|k| k.blank?}
335 keys.collect! {|k| Regexp.escape(k)}
343 keys.collect! {|k| Regexp.escape(k)}
336 format ||= '.+'
344 format ||= '.+'
337 keyword = nil
345 keyword = nil
338 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
346 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
339 if m = text.match(regexp)
347 if m = text.match(regexp)
340 keyword = m[2].strip
348 keyword = m[2].strip
341 text.gsub!(regexp, '')
349 text.gsub!(regexp, '')
342 end
350 end
343 keyword
351 keyword
344 end
352 end
345
353
346 def target_project
354 def target_project
347 # TODO: other ways to specify project:
355 # TODO: other ways to specify project:
348 # * parse the email To field
356 # * parse the email To field
349 # * specific project (eg. Setting.mail_handler_target_project)
357 # * specific project (eg. Setting.mail_handler_target_project)
350 target = Project.find_by_identifier(get_keyword(:project))
358 target = Project.find_by_identifier(get_keyword(:project))
351 if target.nil?
359 if target.nil?
352 # Invalid project keyword, use the project specified as the default one
360 # Invalid project keyword, use the project specified as the default one
353 default_project = @@handler_options[:issue][:project]
361 default_project = @@handler_options[:issue][:project]
354 if default_project.present?
362 if default_project.present?
355 target = Project.find_by_identifier(default_project)
363 target = Project.find_by_identifier(default_project)
356 end
364 end
357 end
365 end
358 raise MissingInformation.new('Unable to determine target project') if target.nil?
366 raise MissingInformation.new('Unable to determine target project') if target.nil?
359 target
367 target
360 end
368 end
361
369
362 # Returns a Hash of issue attributes extracted from keywords in the email body
370 # Returns a Hash of issue attributes extracted from keywords in the email body
363 def issue_attributes_from_keywords(issue)
371 def issue_attributes_from_keywords(issue)
364 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
372 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
365
373
366 attrs = {
374 attrs = {
367 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
375 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
368 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
376 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
369 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
377 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
370 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
378 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
371 'assigned_to_id' => assigned_to.try(:id),
379 'assigned_to_id' => assigned_to.try(:id),
372 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
380 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
373 issue.project.shared_versions.named(k).first.try(:id),
381 issue.project.shared_versions.named(k).first.try(:id),
374 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
382 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
375 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
383 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
376 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
384 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
377 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
385 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
378 }.delete_if {|k, v| v.blank? }
386 }.delete_if {|k, v| v.blank? }
379
387
380 if issue.new_record? && attrs['tracker_id'].nil?
388 if issue.new_record? && attrs['tracker_id'].nil?
381 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
389 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
382 end
390 end
383
391
384 attrs
392 attrs
385 end
393 end
386
394
387 # Returns a Hash of issue custom field values extracted from keywords in the email body
395 # Returns a Hash of issue custom field values extracted from keywords in the email body
388 def custom_field_values_from_keywords(customized)
396 def custom_field_values_from_keywords(customized)
389 customized.custom_field_values.inject({}) do |h, v|
397 customized.custom_field_values.inject({}) do |h, v|
390 if keyword = get_keyword(v.custom_field.name, :override => true)
398 if keyword = get_keyword(v.custom_field.name, :override => true)
391 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
399 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
392 end
400 end
393 h
401 h
394 end
402 end
395 end
403 end
396
404
397 # Returns the text/plain part of the email
405 # Returns the text/plain part of the email
398 # If not found (eg. HTML-only email), returns the body with tags removed
406 # If not found (eg. HTML-only email), returns the body with tags removed
399 def plain_text_body
407 def plain_text_body
400 return @plain_text_body unless @plain_text_body.nil?
408 return @plain_text_body unless @plain_text_body.nil?
401
409
402 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
410 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
403 text_parts
411 text_parts
404 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
412 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
405 html_parts
413 html_parts
406 else
414 else
407 [email]
415 [email]
408 end
416 end
409
417
410 parts.reject! do |part|
418 parts.reject! do |part|
411 part.header[:content_disposition].try(:disposition_type) == 'attachment'
419 part.header[:content_disposition].try(:disposition_type) == 'attachment'
412 end
420 end
413
421
414 @plain_text_body = parts.map do |p|
422 @plain_text_body = parts.map do |p|
415 body_charset = p.charset.respond_to?(:force_encoding) ?
423 body_charset = p.charset.respond_to?(:force_encoding) ?
416 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
424 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
417 Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
425 Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
418 end.join("\r\n")
426 end.join("\r\n")
419
427
420 # strip html tags and remove doctype directive
428 # strip html tags and remove doctype directive
421 if parts.any? {|p| p.mime_type == 'text/html'}
429 if parts.any? {|p| p.mime_type == 'text/html'}
422 @plain_text_body = strip_tags(@plain_text_body.strip)
430 @plain_text_body = strip_tags(@plain_text_body.strip)
423 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
431 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
424 end
432 end
425
433
426 @plain_text_body
434 @plain_text_body
427 end
435 end
428
436
429 def cleaned_up_text_body
437 def cleaned_up_text_body
430 cleanup_body(plain_text_body)
438 cleanup_body(plain_text_body)
431 end
439 end
432
440
433 def cleaned_up_subject
441 def cleaned_up_subject
434 subject = email.subject.to_s
442 subject = email.subject.to_s
435 subject.strip[0,255]
443 subject.strip[0,255]
436 end
444 end
437
445
438 def self.full_sanitizer
446 def self.full_sanitizer
439 @full_sanitizer ||= HTML::FullSanitizer.new
447 @full_sanitizer ||= HTML::FullSanitizer.new
440 end
448 end
441
449
442 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
450 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
443 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
451 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
444 value = value.to_s.slice(0, limit)
452 value = value.to_s.slice(0, limit)
445 object.send("#{attribute}=", value)
453 object.send("#{attribute}=", value)
446 end
454 end
447
455
448 # Returns a User from an email address and a full name
456 # Returns a User from an email address and a full name
449 def self.new_user_from_attributes(email_address, fullname=nil)
457 def self.new_user_from_attributes(email_address, fullname=nil)
450 user = User.new
458 user = User.new
451
459
452 # Truncating the email address would result in an invalid format
460 # Truncating the email address would result in an invalid format
453 user.mail = email_address
461 user.mail = email_address
454 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
462 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
455
463
456 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
464 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
457 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
465 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
458 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
466 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
459 user.lastname = '-' if user.lastname.blank?
467 user.lastname = '-' if user.lastname.blank?
460 user.language = Setting.default_language
468 user.language = Setting.default_language
461 user.generate_password = true
469 user.generate_password = true
462 user.mail_notification = 'only_my_events'
470 user.mail_notification = 'only_my_events'
463
471
464 unless user.valid?
472 unless user.valid?
465 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
473 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
466 user.firstname = "-" unless user.errors[:firstname].blank?
474 user.firstname = "-" unless user.errors[:firstname].blank?
467 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
475 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
468 end
476 end
469
477
470 user
478 user
471 end
479 end
472
480
473 # Creates a User for the +email+ sender
481 # Creates a User for the +email+ sender
474 # Returns the user or nil if it could not be created
482 # Returns the user or nil if it could not be created
475 def create_user_from_email
483 def create_user_from_email
476 from = email.header['from'].to_s
484 from = email.header['from'].to_s
477 addr, name = from, nil
485 addr, name = from, nil
478 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
486 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
479 addr, name = m[2], m[1]
487 addr, name = m[2], m[1]
480 end
488 end
481 if addr.present?
489 if addr.present?
482 user = self.class.new_user_from_attributes(addr, name)
490 user = self.class.new_user_from_attributes(addr, name)
483 if @@handler_options[:no_notification]
491 if @@handler_options[:no_notification]
484 user.mail_notification = 'none'
492 user.mail_notification = 'none'
485 end
493 end
486 if user.save
494 if user.save
487 user
495 user
488 else
496 else
489 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
497 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
490 nil
498 nil
491 end
499 end
492 else
500 else
493 logger.error "MailHandler: failed to create User: no FROM address found" if logger
501 logger.error "MailHandler: failed to create User: no FROM address found" if logger
494 nil
502 nil
495 end
503 end
496 end
504 end
497
505
498 # Adds the newly created user to default group
506 # Adds the newly created user to default group
499 def add_user_to_group(default_group)
507 def add_user_to_group(default_group)
500 if default_group.present?
508 if default_group.present?
501 default_group.split(',').each do |group_name|
509 default_group.split(',').each do |group_name|
502 if group = Group.named(group_name).first
510 if group = Group.named(group_name).first
503 group.users << @user
511 group.users << @user
504 elsif logger
512 elsif logger
505 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
513 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
506 end
514 end
507 end
515 end
508 end
516 end
509 end
517 end
510
518
511 # Removes the email body of text after the truncation configurations.
519 # Removes the email body of text after the truncation configurations.
512 def cleanup_body(body)
520 def cleanup_body(body)
513 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
521 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
514 unless delimiters.empty?
522 unless delimiters.empty?
515 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
523 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
516 body = body.gsub(regex, '')
524 body = body.gsub(regex, '')
517 end
525 end
518 body.strip
526 body.strip
519 end
527 end
520
528
521 def find_assignee_from_keyword(keyword, issue)
529 def find_assignee_from_keyword(keyword, issue)
522 keyword = keyword.to_s.downcase
530 keyword = keyword.to_s.downcase
523 assignable = issue.assignable_users
531 assignable = issue.assignable_users
524 assignee = nil
532 assignee = nil
525 assignee ||= assignable.detect {|a|
533 assignee ||= assignable.detect {|a|
526 a.mail.to_s.downcase == keyword ||
534 a.mail.to_s.downcase == keyword ||
527 a.login.to_s.downcase == keyword
535 a.login.to_s.downcase == keyword
528 }
536 }
529 if assignee.nil? && keyword.match(/ /)
537 if assignee.nil? && keyword.match(/ /)
530 firstname, lastname = *(keyword.split) # "First Last Throwaway"
538 firstname, lastname = *(keyword.split) # "First Last Throwaway"
531 assignee ||= assignable.detect {|a|
539 assignee ||= assignable.detect {|a|
532 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
540 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
533 a.lastname.to_s.downcase == lastname
541 a.lastname.to_s.downcase == lastname
534 }
542 }
535 end
543 end
536 if assignee.nil?
544 if assignee.nil?
537 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
545 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
538 end
546 end
539 assignee
547 assignee
540 end
548 end
541 end
549 end
@@ -1,62 +1,62
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'net/imap'
18 require 'net/imap'
19
19
20 module Redmine
20 module Redmine
21 module IMAP
21 module IMAP
22 class << self
22 class << self
23 def check(imap_options={}, options={})
23 def check(imap_options={}, options={})
24 host = imap_options[:host] || '127.0.0.1'
24 host = imap_options[:host] || '127.0.0.1'
25 port = imap_options[:port] || '143'
25 port = imap_options[:port] || '143'
26 ssl = !imap_options[:ssl].nil?
26 ssl = !imap_options[:ssl].nil?
27 folder = imap_options[:folder] || 'INBOX'
27 folder = imap_options[:folder] || 'INBOX'
28
28
29 imap = Net::IMAP.new(host, port, ssl)
29 imap = Net::IMAP.new(host, port, ssl)
30 imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil?
30 imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil?
31 imap.select(folder)
31 imap.select(folder)
32 imap.uid_search(['NOT', 'SEEN']).each do |uid|
32 imap.uid_search(['NOT', 'SEEN']).each do |uid|
33 msg = imap.uid_fetch(uid,'RFC822')[0].attr['RFC822']
33 msg = imap.uid_fetch(uid,'RFC822')[0].attr['RFC822']
34 logger.debug "Receiving message #{uid}" if logger && logger.debug?
34 logger.debug "Receiving message #{uid}" if logger && logger.debug?
35 if MailHandler.receive(msg, options)
35 if MailHandler.safe_receive(msg, options)
36 logger.debug "Message #{uid} successfully received" if logger && logger.debug?
36 logger.debug "Message #{uid} successfully received" if logger && logger.debug?
37 if imap_options[:move_on_success]
37 if imap_options[:move_on_success]
38 imap.uid_copy(uid, imap_options[:move_on_success])
38 imap.uid_copy(uid, imap_options[:move_on_success])
39 end
39 end
40 imap.uid_store(uid, "+FLAGS", [:Seen, :Deleted])
40 imap.uid_store(uid, "+FLAGS", [:Seen, :Deleted])
41 else
41 else
42 logger.debug "Message #{uid} can not be processed" if logger && logger.debug?
42 logger.debug "Message #{uid} can not be processed" if logger && logger.debug?
43 imap.uid_store(uid, "+FLAGS", [:Seen])
43 imap.uid_store(uid, "+FLAGS", [:Seen])
44 if imap_options[:move_on_failure]
44 if imap_options[:move_on_failure]
45 imap.uid_copy(uid, imap_options[:move_on_failure])
45 imap.uid_copy(uid, imap_options[:move_on_failure])
46 imap.uid_store(uid, "+FLAGS", [:Deleted])
46 imap.uid_store(uid, "+FLAGS", [:Deleted])
47 end
47 end
48 end
48 end
49 end
49 end
50 imap.expunge
50 imap.expunge
51 imap.logout
51 imap.logout
52 imap.disconnect
52 imap.disconnect
53 end
53 end
54
54
55 private
55 private
56
56
57 def logger
57 def logger
58 ::Rails.logger
58 ::Rails.logger
59 end
59 end
60 end
60 end
61 end
61 end
62 end
62 end
@@ -1,62 +1,62
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'net/pop'
18 require 'net/pop'
19
19
20 module Redmine
20 module Redmine
21 module POP3
21 module POP3
22 class << self
22 class << self
23 def check(pop_options={}, options={})
23 def check(pop_options={}, options={})
24 host = pop_options[:host] || '127.0.0.1'
24 host = pop_options[:host] || '127.0.0.1'
25 port = pop_options[:port] || '110'
25 port = pop_options[:port] || '110'
26 apop = (pop_options[:apop].to_s == '1')
26 apop = (pop_options[:apop].to_s == '1')
27 delete_unprocessed = (pop_options[:delete_unprocessed].to_s == '1')
27 delete_unprocessed = (pop_options[:delete_unprocessed].to_s == '1')
28
28
29 pop = Net::POP3.APOP(apop).new(host,port)
29 pop = Net::POP3.APOP(apop).new(host,port)
30 logger.debug "Connecting to #{host}..." if logger && logger.debug?
30 logger.debug "Connecting to #{host}..." if logger && logger.debug?
31 pop.start(pop_options[:username], pop_options[:password]) do |pop_session|
31 pop.start(pop_options[:username], pop_options[:password]) do |pop_session|
32 if pop_session.mails.empty?
32 if pop_session.mails.empty?
33 logger.debug "No email to process" if logger && logger.debug?
33 logger.debug "No email to process" if logger && logger.debug?
34 else
34 else
35 logger.debug "#{pop_session.mails.size} email(s) to process..." if logger && logger.debug?
35 logger.debug "#{pop_session.mails.size} email(s) to process..." if logger && logger.debug?
36 pop_session.each_mail do |msg|
36 pop_session.each_mail do |msg|
37 message = msg.pop
37 message = msg.pop
38 message_id = (message =~ /^Message-I[dD]: (.*)/ ? $1 : '').strip
38 message_id = (message =~ /^Message-I[dD]: (.*)/ ? $1 : '').strip
39 if MailHandler.receive(message, options)
39 if MailHandler.safe_receive(message, options)
40 msg.delete
40 msg.delete
41 logger.debug "--> Message #{message_id} processed and deleted from the server" if logger && logger.debug?
41 logger.debug "--> Message #{message_id} processed and deleted from the server" if logger && logger.debug?
42 else
42 else
43 if delete_unprocessed
43 if delete_unprocessed
44 msg.delete
44 msg.delete
45 logger.debug "--> Message #{message_id} NOT processed and deleted from the server" if logger && logger.debug?
45 logger.debug "--> Message #{message_id} NOT processed and deleted from the server" if logger && logger.debug?
46 else
46 else
47 logger.debug "--> Message #{message_id} NOT processed and left on the server" if logger && logger.debug?
47 logger.debug "--> Message #{message_id} NOT processed and left on the server" if logger && logger.debug?
48 end
48 end
49 end
49 end
50 end
50 end
51 end
51 end
52 end
52 end
53 end
53 end
54
54
55 private
55 private
56
56
57 def logger
57 def logger
58 ::Rails.logger
58 ::Rails.logger
59 end
59 end
60 end
60 end
61 end
61 end
62 end
62 end
General Comments 0
You need to be logged in to leave comments. Login now