##// END OF EJS Templates
rcmail: pass smtplib.SMTP.sendmail to_addrs as list...
Mads Kiilerich -
r8380:9c408c0f default
parent child Browse files
Show More
@@ -1,106 +1,106
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.rcmail.smtp_mailer
15 kallithea.lib.rcmail.smtp_mailer
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Simple smtp mailer used in Kallithea
18 Simple smtp mailer used in Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Sep 13, 2010
22 :created_on: Sep 13, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import smtplib
29 import smtplib
30 import time
30 import time
31 from email.utils import formatdate
31 from email.utils import formatdate
32 from ssl import SSLError
32 from ssl import SSLError
33
33
34 from kallithea.lib.rcmail.message import Message
34 from kallithea.lib.rcmail.message import Message
35 from kallithea.lib.rcmail.utils import DNS_NAME
35 from kallithea.lib.rcmail.utils import DNS_NAME
36
36
37
37
38 class SmtpMailer(object):
38 class SmtpMailer(object):
39 """SMTP mailer class
39 """SMTP mailer class
40
40
41 mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
41 mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
42 mail_port, ssl, tls)
42 mail_port, ssl, tls)
43 mailer.send(recipients, subject, body, attachment_files)
43 mailer.send(recipients, subject, body, attachment_files)
44
44
45 :param recipients might be a list of string or single string
45 :param recipients might be a list of string or single string
46 :param attachment_files is a dict of {filename:location}
46 :param attachment_files is a dict of {filename:location}
47 it tries to guess the mimetype and attach the file
47 it tries to guess the mimetype and attach the file
48
48
49 """
49 """
50
50
51 def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
51 def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
52 mail_port=None, ssl=False, tls=False, debug=False):
52 mail_port=None, ssl=False, tls=False, debug=False):
53
53
54 self.mail_from = mail_from
54 self.mail_from = mail_from
55 self.mail_server = mail_server
55 self.mail_server = mail_server
56 self.mail_port = mail_port
56 self.mail_port = mail_port
57 self.user = user
57 self.user = user
58 self.passwd = passwd
58 self.passwd = passwd
59 self.ssl = ssl
59 self.ssl = ssl
60 self.tls = tls
60 self.tls = tls
61 self.debug = debug
61 self.debug = debug
62 self.auth = smtp_auth
62 self.auth = smtp_auth
63
63
64 def send(self, recipients=None, subject='', body='', html='',
64 def send(self, recipients=None, subject='', body='', html='',
65 attachment_files=None, headers=None):
65 attachment_files=None, headers=None):
66 recipients = recipients or []
66 recipients = recipients or []
67 if isinstance(recipients, str):
67 if isinstance(recipients, str):
68 recipients = [recipients]
68 recipients = [recipients]
69 if headers is None:
69 if headers is None:
70 headers = {}
70 headers = {}
71 headers.setdefault('Date', formatdate(time.time()))
71 headers.setdefault('Date', formatdate(time.time()))
72 msg = Message(subject, recipients, body, html, self.mail_from,
72 msg = Message(subject, recipients, body, html, self.mail_from,
73 recipients_separator=", ", extra_headers=headers)
73 recipients_separator=", ", extra_headers=headers)
74 raw_msg = msg.to_message()
74 raw_msg = msg.to_message()
75
75
76 if self.ssl:
76 if self.ssl:
77 smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port,
77 smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port,
78 local_hostname=DNS_NAME.get_fqdn())
78 local_hostname=DNS_NAME.get_fqdn())
79 else:
79 else:
80 smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port,
80 smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port,
81 local_hostname=DNS_NAME.get_fqdn())
81 local_hostname=DNS_NAME.get_fqdn())
82
82
83 if self.tls:
83 if self.tls:
84 smtp_serv.ehlo()
84 smtp_serv.ehlo()
85 smtp_serv.starttls()
85 smtp_serv.starttls()
86
86
87 if self.debug:
87 if self.debug:
88 smtp_serv.set_debuglevel(1)
88 smtp_serv.set_debuglevel(1)
89
89
90 smtp_serv.ehlo()
90 smtp_serv.ehlo()
91 if self.auth:
91 if self.auth:
92 smtp_serv.esmtp_features["auth"] = self.auth
92 smtp_serv.esmtp_features["auth"] = self.auth
93
93
94 # if server requires authorization you must provide login and password
94 # if server requires authorization you must provide login and password
95 # but only if we have them
95 # but only if we have them
96 if self.user and self.passwd:
96 if self.user and self.passwd:
97 smtp_serv.login(self.user, self.passwd)
97 smtp_serv.login(self.user, self.passwd)
98
98
99 smtp_serv.sendmail(msg.sender, msg.send_to, raw_msg.as_string())
99 smtp_serv.sendmail(msg.sender, list(msg.send_to), raw_msg.as_string())
100 logging.info('MAIL SENT TO: %s' % recipients)
100 logging.info('MAIL SENT TO: %s' % recipients)
101
101
102 try:
102 try:
103 smtp_serv.quit()
103 smtp_serv.quit()
104 except SSLError:
104 except SSLError:
105 # SSL error might sometimes be raised in tls connections on closing
105 # SSL error might sometimes be raised in tls connections on closing
106 smtp_serv.close()
106 smtp_serv.close()
@@ -1,197 +1,196
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 import mock
3 import mock
4
4
5 import kallithea
5 import kallithea
6 from kallithea.model.db import User
6 from kallithea.model.db import User
7 from kallithea.tests import base
7 from kallithea.tests import base
8
8
9
9
10 class smtplib_mock(object):
10 class smtplib_mock(object):
11
11
12 @classmethod
12 @classmethod
13 def SMTP(cls, server, port, local_hostname):
13 def SMTP(cls, server, port, local_hostname):
14 return smtplib_mock()
14 return smtplib_mock()
15
15
16 def ehlo(self):
16 def ehlo(self):
17 pass
17 pass
18
18
19 def quit(self):
19 def quit(self):
20 pass
20 pass
21
21
22 def sendmail(self, sender, dest, msg):
22 def sendmail(self, sender, dest, msg):
23 smtplib_mock.lastsender = sender
23 smtplib_mock.lastsender = sender
24 smtplib_mock.lastdest = dest
24 smtplib_mock.lastdest = set(dest)
25 smtplib_mock.lastmsg = msg
25 smtplib_mock.lastmsg = msg
26 pass
27
26
28
27
29 @mock.patch('kallithea.lib.rcmail.smtp_mailer.smtplib', smtplib_mock)
28 @mock.patch('kallithea.lib.rcmail.smtp_mailer.smtplib', smtplib_mock)
30 class TestMail(base.TestController):
29 class TestMail(base.TestController):
31
30
32 def test_send_mail_trivial(self):
31 def test_send_mail_trivial(self):
33 mailserver = 'smtp.mailserver.org'
32 mailserver = 'smtp.mailserver.org'
34 recipients = ['rcpt1', 'rcpt2']
33 recipients = ['rcpt1', 'rcpt2']
35 envelope_from = 'noreply@mailserver.org'
34 envelope_from = 'noreply@mailserver.org'
36 subject = 'subject'
35 subject = 'subject'
37 body = 'body'
36 body = 'body'
38 html_body = 'html_body'
37 html_body = 'html_body'
39
38
40 config_mock = {
39 config_mock = {
41 'smtp_server': mailserver,
40 'smtp_server': mailserver,
42 'app_email_from': envelope_from,
41 'app_email_from': envelope_from,
43 }
42 }
44 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
43 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
45 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
44 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
46
45
47 assert smtplib_mock.lastdest == set(recipients)
46 assert smtplib_mock.lastdest == set(recipients)
48 assert smtplib_mock.lastsender == envelope_from
47 assert smtplib_mock.lastsender == envelope_from
49 assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
48 assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
50 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
49 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
51 assert body in smtplib_mock.lastmsg
50 assert body in smtplib_mock.lastmsg
52 assert html_body in smtplib_mock.lastmsg
51 assert html_body in smtplib_mock.lastmsg
53
52
54 def test_send_mail_no_recipients(self):
53 def test_send_mail_no_recipients(self):
55 mailserver = 'smtp.mailserver.org'
54 mailserver = 'smtp.mailserver.org'
56 recipients = []
55 recipients = []
57 envelope_from = 'noreply@mailserver.org'
56 envelope_from = 'noreply@mailserver.org'
58 email_to = 'admin@mailserver.org'
57 email_to = 'admin@mailserver.org'
59 subject = 'subject'
58 subject = 'subject'
60 body = 'body'
59 body = 'body'
61 html_body = 'html_body'
60 html_body = 'html_body'
62
61
63 config_mock = {
62 config_mock = {
64 'smtp_server': mailserver,
63 'smtp_server': mailserver,
65 'app_email_from': envelope_from,
64 'app_email_from': envelope_from,
66 'email_to': email_to,
65 'email_to': email_to,
67 }
66 }
68 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
67 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
69 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
68 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
70
69
71 assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL, email_to])
70 assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL, email_to])
72 assert smtplib_mock.lastsender == envelope_from
71 assert smtplib_mock.lastsender == envelope_from
73 assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
72 assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
74 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
73 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
75 assert body in smtplib_mock.lastmsg
74 assert body in smtplib_mock.lastmsg
76 assert html_body in smtplib_mock.lastmsg
75 assert html_body in smtplib_mock.lastmsg
77
76
78 def test_send_mail_no_recipients_multiple_email_to(self):
77 def test_send_mail_no_recipients_multiple_email_to(self):
79 mailserver = 'smtp.mailserver.org'
78 mailserver = 'smtp.mailserver.org'
80 recipients = []
79 recipients = []
81 envelope_from = 'noreply@mailserver.org'
80 envelope_from = 'noreply@mailserver.org'
82 email_to = 'admin@mailserver.org,admin2@example.com'
81 email_to = 'admin@mailserver.org,admin2@example.com'
83 subject = 'subject'
82 subject = 'subject'
84 body = 'body'
83 body = 'body'
85 html_body = 'html_body'
84 html_body = 'html_body'
86
85
87 config_mock = {
86 config_mock = {
88 'smtp_server': mailserver,
87 'smtp_server': mailserver,
89 'app_email_from': envelope_from,
88 'app_email_from': envelope_from,
90 'email_to': email_to,
89 'email_to': email_to,
91 }
90 }
92 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
91 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
93 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
92 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
94
93
95 assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL] + email_to.split(','))
94 assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL] + email_to.split(','))
96 assert smtplib_mock.lastsender == envelope_from
95 assert smtplib_mock.lastsender == envelope_from
97 assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
96 assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
98 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
97 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
99 assert body in smtplib_mock.lastmsg
98 assert body in smtplib_mock.lastmsg
100 assert html_body in smtplib_mock.lastmsg
99 assert html_body in smtplib_mock.lastmsg
101
100
102 def test_send_mail_no_recipients_no_email_to(self):
101 def test_send_mail_no_recipients_no_email_to(self):
103 mailserver = 'smtp.mailserver.org'
102 mailserver = 'smtp.mailserver.org'
104 recipients = []
103 recipients = []
105 envelope_from = 'noreply@mailserver.org'
104 envelope_from = 'noreply@mailserver.org'
106 subject = 'subject'
105 subject = 'subject'
107 body = 'body'
106 body = 'body'
108 html_body = 'html_body'
107 html_body = 'html_body'
109
108
110 config_mock = {
109 config_mock = {
111 'smtp_server': mailserver,
110 'smtp_server': mailserver,
112 'app_email_from': envelope_from,
111 'app_email_from': envelope_from,
113 }
112 }
114 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
113 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
115 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
114 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
116
115
117 assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL])
116 assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL])
118 assert smtplib_mock.lastsender == envelope_from
117 assert smtplib_mock.lastsender == envelope_from
119 assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
118 assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
120 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
119 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
121 assert body in smtplib_mock.lastmsg
120 assert body in smtplib_mock.lastmsg
122 assert html_body in smtplib_mock.lastmsg
121 assert html_body in smtplib_mock.lastmsg
123
122
124 def test_send_mail_with_author(self):
123 def test_send_mail_with_author(self):
125 mailserver = 'smtp.mailserver.org'
124 mailserver = 'smtp.mailserver.org'
126 recipients = ['rcpt1', 'rcpt2']
125 recipients = ['rcpt1', 'rcpt2']
127 envelope_from = 'noreply@mailserver.org'
126 envelope_from = 'noreply@mailserver.org'
128 subject = 'subject'
127 subject = 'subject'
129 body = 'body'
128 body = 'body'
130 html_body = 'html_body'
129 html_body = 'html_body'
131 author = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
130 author = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
132
131
133 config_mock = {
132 config_mock = {
134 'smtp_server': mailserver,
133 'smtp_server': mailserver,
135 'app_email_from': envelope_from,
134 'app_email_from': envelope_from,
136 }
135 }
137 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
136 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
138 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
137 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
139
138
140 assert smtplib_mock.lastdest == set(recipients)
139 assert smtplib_mock.lastdest == set(recipients)
141 assert smtplib_mock.lastsender == envelope_from
140 assert smtplib_mock.lastsender == envelope_from
142 assert 'From: "Kallithea Admin (no-reply)" <%s>' % envelope_from in smtplib_mock.lastmsg
141 assert 'From: "Kallithea Admin (no-reply)" <%s>' % envelope_from in smtplib_mock.lastmsg
143 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
142 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
144 assert body in smtplib_mock.lastmsg
143 assert body in smtplib_mock.lastmsg
145 assert html_body in smtplib_mock.lastmsg
144 assert html_body in smtplib_mock.lastmsg
146
145
147 def test_send_mail_with_author_full_mail_from(self):
146 def test_send_mail_with_author_full_mail_from(self):
148 mailserver = 'smtp.mailserver.org'
147 mailserver = 'smtp.mailserver.org'
149 recipients = ['ræcpt1', 'receptor2 <rcpt2@example.com>', 'tæst@example.com', 'Tæst <test@example.com>']
148 recipients = ['ræcpt1', 'receptor2 <rcpt2@example.com>', 'tæst@example.com', 'Tæst <test@example.com>']
150 envelope_addr = 'noreply@mailserver.org'
149 envelope_addr = 'noreply@mailserver.org'
151 envelope_from = 'Sâme Næme <%s>' % envelope_addr
150 envelope_from = 'Sâme Næme <%s>' % envelope_addr
152 subject = 'subject'
151 subject = 'subject'
153 body = 'body'
152 body = 'body'
154 html_body = 'html_body'
153 html_body = 'html_body'
155 author = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
154 author = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
156
155
157 config_mock = {
156 config_mock = {
158 'smtp_server': mailserver,
157 'smtp_server': mailserver,
159 'app_email_from': envelope_from,
158 'app_email_from': envelope_from,
160 }
159 }
161 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
160 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
162 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
161 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
163
162
164 assert smtplib_mock.lastdest == set(recipients)
163 assert smtplib_mock.lastdest == set(recipients)
165 assert smtplib_mock.lastsender == envelope_from
164 assert smtplib_mock.lastsender == envelope_from
166 assert 'From: "Kallithea Admin (no-reply)" <%s>' % envelope_addr in smtplib_mock.lastmsg
165 assert 'From: "Kallithea Admin (no-reply)" <%s>' % envelope_addr in smtplib_mock.lastmsg
167 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
166 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
168 assert body in smtplib_mock.lastmsg
167 assert body in smtplib_mock.lastmsg
169 assert html_body in smtplib_mock.lastmsg
168 assert html_body in smtplib_mock.lastmsg
170
169
171 def test_send_mail_extra_headers(self):
170 def test_send_mail_extra_headers(self):
172 mailserver = 'smtp.mailserver.org'
171 mailserver = 'smtp.mailserver.org'
173 recipients = ['rcpt1', 'rcpt2']
172 recipients = ['rcpt1', 'rcpt2']
174 envelope_from = 'noreply@mailserver.org'
173 envelope_from = 'noreply@mailserver.org'
175 subject = 'subject'
174 subject = 'subject'
176 body = 'body'
175 body = 'body'
177 html_body = 'html_body'
176 html_body = 'html_body'
178 author = User(name='foo', lastname='(fubar) "baz"')
177 author = User(name='foo', lastname='(fubar) "baz"')
179 headers = {'extra': 'yes'}
178 headers = {'extra': 'yes'}
180
179
181 config_mock = {
180 config_mock = {
182 'smtp_server': mailserver,
181 'smtp_server': mailserver,
183 'app_email_from': envelope_from,
182 'app_email_from': envelope_from,
184 }
183 }
185 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
184 with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
186 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body,
185 kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body,
187 from_name=author.full_name_or_username, headers=headers)
186 from_name=author.full_name_or_username, headers=headers)
188
187
189 assert smtplib_mock.lastdest == set(recipients)
188 assert smtplib_mock.lastdest == set(recipients)
190 assert smtplib_mock.lastsender == envelope_from
189 assert smtplib_mock.lastsender == envelope_from
191 assert r'From: "foo (fubar) \"baz\" (no-reply)" <%s>' % envelope_from in smtplib_mock.lastmsg
190 assert r'From: "foo (fubar) \"baz\" (no-reply)" <%s>' % envelope_from in smtplib_mock.lastmsg
192 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
191 assert 'Subject: %s' % subject in smtplib_mock.lastmsg
193 assert body in smtplib_mock.lastmsg
192 assert body in smtplib_mock.lastmsg
194 assert html_body in smtplib_mock.lastmsg
193 assert html_body in smtplib_mock.lastmsg
195 assert 'Extra: yes' in smtplib_mock.lastmsg
194 assert 'Extra: yes' in smtplib_mock.lastmsg
196 # verify that headers dict hasn't mutated by send_email
195 # verify that headers dict hasn't mutated by send_email
197 assert headers == {'extra': 'yes'}
196 assert headers == {'extra': 'yes'}
General Comments 0
You need to be logged in to leave comments. Login now