Avoid potential negative array index access to cached text.
[LibreOffice.git] / scripting / source / pyprov / mailmerge.py
blob3c781c52f2cb25b69582daa07f187b3d1aa21966
1 # Caolan McNamara caolanm@redhat.com
2 # a simple email mailmerge component
4 # manual installation for hackers, not necessary for users
5 # cp mailmerge.py /usr/lib/libreoffice/program
6 # cd /usr/lib/libreoffice/program
7 # ./unopkg add --shared mailmerge.py
8 # edit ~/.openoffice.org2/user/registry/data/org/openoffice/Office/Writer.xcu
9 # and change EMailSupported to as follows...
10 # <prop oor:name="EMailSupported" oor:type="xs:boolean">
11 # <value>true</value>
12 # </prop>
14 import unohelper
15 import uno
16 import re
17 import os
18 import encodings.idna
20 #to implement com::sun::star::mail::XMailServiceProvider
21 #and
22 #to implement com.sun.star.mail.XMailMessage
24 from com.sun.star.mail import XMailServiceProvider
25 from com.sun.star.mail import XMailService
26 from com.sun.star.mail import XSmtpService
27 from com.sun.star.mail import XConnectionListener
28 from com.sun.star.mail import XAuthenticator
29 from com.sun.star.mail import XMailMessage
30 from com.sun.star.mail.MailServiceType import SMTP
31 from com.sun.star.mail.MailServiceType import POP3
32 from com.sun.star.mail.MailServiceType import IMAP
33 from com.sun.star.uno import XCurrentContext
34 from com.sun.star.lang import IllegalArgumentException
35 from com.sun.star.lang import EventObject
36 from com.sun.star.lang import XServiceInfo
37 from com.sun.star.mail import SendMailMessageFailedException
39 from email.mime.base import MIMEBase
40 from email.message import Message
41 from email.charset import Charset
42 from email.charset import QP
43 from email.encoders import encode_base64
44 from email.header import Header
45 from email.mime.multipart import MIMEMultipart
46 from email.utils import formatdate
47 from email.utils import parseaddr
48 from socket import _GLOBAL_DEFAULT_TIMEOUT
50 import sys, ssl, smtplib, imaplib, poplib
51 dbg = False
53 # pythonloader looks for a static g_ImplementationHelper variable
54 g_ImplementationHelper = unohelper.ImplementationHelper()
55 g_providerImplName = "org.openoffice.pyuno.MailServiceProvider"
56 g_messageImplName = "org.openoffice.pyuno.MailMessage"
58 def prepareTLSContext(xComponent, xContext, isTLSRequested):
59 xConfigProvider = xContext.ServiceManager.createInstance("com.sun.star.configuration.ConfigurationProvider")
60 prop = uno.createUnoStruct('com.sun.star.beans.PropertyValue')
61 prop.Name = "nodepath"
62 prop.Value = "/org.openoffice.Office.Security/Net"
63 xSettings = xConfigProvider.createInstanceWithArguments("com.sun.star.configuration.ConfigurationAccess",
64 (prop,))
65 isAllowedInsecure = xSettings.getByName("AllowInsecureProtocols")
66 tlscontext = None
67 if isTLSRequested:
68 if dbg:
69 print("SSL config: " + str(ssl.get_default_verify_paths()), file=sys.stderr)
70 tlscontext = ssl.create_default_context()
71 # SSLv2/v3 is already disabled by default.
72 # This check does not work, because OpenSSL 3 defines SSL_OP_NO_SSLv2
73 # as 0, so even though _ssl__SSLContext_impl() tries to set it,
74 # getting the value from SSL_CTX_get_options() doesn't lead to setting
75 # the python-level flag.
76 #assert (tlscontext.options & ssl.Options.OP_NO_SSLv2) != 0
77 assert (tlscontext.options & ssl.Options.OP_NO_SSLv3) != 0
78 if not(isAllowedInsecure):
79 if not(isTLSRequested):
80 if dbg:
81 print("mailmerge.py: insecure connection not allowed by configuration", file=sys.stderr)
82 raise IllegalArgumentException("insecure connection not allowed by configuration", xComponent, 1)
83 tlscontext.options |= ssl.Options.OP_NO_TLSv1 | ssl.Options.OP_NO_TLSv1_1
84 return tlscontext
86 class PyMailSMTPService(unohelper.Base, XSmtpService):
87 def __init__( self, ctx ):
88 self.ctx = ctx
89 self.listeners = []
90 self.supportedtypes = ('Insecure', 'Ssl')
91 self.server = None
92 self.connectioncontext = None
93 self.notify = EventObject(self)
94 if dbg:
95 print("PyMailSMTPService init", file=sys.stderr)
96 print("python version is: " + sys.version, file=sys.stderr)
97 def addConnectionListener(self, xListener):
98 if dbg:
99 print("PyMailSMTPService addConnectionListener", file=sys.stderr)
100 self.listeners.append(xListener)
101 def removeConnectionListener(self, xListener):
102 if dbg:
103 print("PyMailSMTPService removeConnectionListener", file=sys.stderr)
104 self.listeners.remove(xListener)
105 def getSupportedConnectionTypes(self):
106 if dbg:
107 print("PyMailSMTPService getSupportedConnectionTypes", file=sys.stderr)
108 return self.supportedtypes
109 def connect(self, xConnectionContext, xAuthenticator):
110 self.connectioncontext = xConnectionContext
111 if dbg:
112 print("PyMailSMTPService connect", file=sys.stderr)
113 server = xConnectionContext.getValueByName("ServerName").strip()
114 if dbg:
115 print("ServerName: " + server, file=sys.stderr)
116 port = int(xConnectionContext.getValueByName("Port"))
117 if dbg:
118 print("Port: " + str(port), file=sys.stderr)
119 tout = xConnectionContext.getValueByName("Timeout")
120 if dbg:
121 print(isinstance(tout,int), file=sys.stderr)
122 if not isinstance(tout,int):
123 tout = _GLOBAL_DEFAULT_TIMEOUT
124 if dbg:
125 print("Timeout: " + str(tout), file=sys.stderr)
126 connectiontype = xConnectionContext.getValueByName("ConnectionType")
127 if dbg:
128 print("ConnectionType: " + connectiontype, file=sys.stderr)
129 tlscontext = prepareTLSContext(self, self.ctx, connectiontype.upper() == 'SSL' or port == 465)
130 if port == 465:
131 self.server = smtplib.SMTP_SSL(server, port, timeout=tout, context=tlscontext)
132 else:
133 self.server = smtplib.SMTP(server, port,timeout=tout)
135 if dbg:
136 self.server.set_debuglevel(1)
138 if connectiontype.upper() == 'SSL' and port != 465:
139 # STRIPTLS: smtplib raises an exception if result is not 220
140 self.server.starttls(context=tlscontext)
142 user = xAuthenticator.getUserName()
143 password = xAuthenticator.getPassword()
144 if user != '':
145 if dbg:
146 print("Logging in, username of: " + user, file=sys.stderr)
147 self.server.login(user, password)
149 for listener in self.listeners:
150 listener.connected(self.notify)
151 def disconnect(self):
152 if dbg:
153 print("PyMailSMTPService disconnect", file=sys.stderr)
154 if self.server:
155 self.server.quit()
156 self.server = None
157 for listener in self.listeners:
158 listener.disconnected(self.notify)
159 def isConnected(self):
160 if dbg:
161 print("PyMailSMTPService isConnected", file=sys.stderr)
162 return self.server != None
163 def getCurrentConnectionContext(self):
164 if dbg:
165 print("PyMailSMTPService getCurrentConnectionContext", file=sys.stderr)
166 return self.connectioncontext
167 def sendMailMessage(self, xMailMessage):
168 COMMASPACE = ', '
170 if dbg:
171 print("PyMailSMTPService sendMailMessage", file=sys.stderr)
172 recipients = xMailMessage.getRecipients()
173 sendermail = xMailMessage.SenderAddress
174 sendername = xMailMessage.SenderName
175 subject = xMailMessage.Subject
176 ccrecipients = xMailMessage.getCcRecipients()
177 bccrecipients = xMailMessage.getBccRecipients()
178 if dbg:
179 print("PyMailSMTPService subject: " + subject, file=sys.stderr)
180 print("PyMailSMTPService from: " + sendername, file=sys.stderr)
181 print("PyMailSMTPService from: " + sendermail, file=sys.stderr)
182 print("PyMailSMTPService send to: %s" % (recipients,), file=sys.stderr)
184 attachments = xMailMessage.getAttachments()
186 textmsg = Message()
188 content = xMailMessage.Body
189 flavors = content.getTransferDataFlavors()
190 if dbg:
191 print("PyMailSMTPService flavors len: %d" % (len(flavors),), file=sys.stderr)
193 #Use first flavor that's sane for an email body
194 for flavor in flavors:
195 if flavor.MimeType.find('text/html') != -1 or flavor.MimeType.find('text/plain') != -1:
196 if dbg:
197 print("PyMailSMTPService mimetype is: " + flavor.MimeType, file=sys.stderr)
198 textbody = content.getTransferData(flavor)
200 if len(textbody):
201 mimeEncoding = re.sub("charset=.*", "charset=UTF-8", flavor.MimeType)
202 if mimeEncoding.find('charset=UTF-8') == -1:
203 mimeEncoding = mimeEncoding + "; charset=UTF-8"
204 textmsg['Content-Type'] = mimeEncoding
205 textmsg['MIME-Version'] = '1.0'
207 try:
208 #it's a string, get it as utf-8 bytes
209 textbody = textbody.encode('utf-8')
210 except:
211 #it's a bytesequence, get raw bytes
212 textbody = textbody.value
213 textbody = textbody.decode('utf-8')
214 c = Charset('utf-8')
215 c.body_encoding = QP
216 textmsg.set_payload(textbody, c)
218 break
220 if (len(attachments)):
221 msg = MIMEMultipart()
222 msg.epilogue = ''
223 msg.attach(textmsg)
224 else:
225 msg = textmsg
227 hdr = Header(sendername, 'utf-8')
228 hdr.append('<'+sendermail+'>','us-ascii')
229 msg['Subject'] = subject
230 msg['From'] = hdr
231 msg['To'] = COMMASPACE.join(recipients)
232 if len(ccrecipients):
233 msg['Cc'] = COMMASPACE.join(ccrecipients)
234 if xMailMessage.ReplyToAddress != '':
235 msg['Reply-To'] = xMailMessage.ReplyToAddress
237 mailerstring = "LibreOffice via Caolan's mailmerge component"
238 try:
239 ctx = uno.getComponentContext()
240 aConfigProvider = ctx.ServiceManager.createInstance("com.sun.star.configuration.ConfigurationProvider")
241 prop = uno.createUnoStruct('com.sun.star.beans.PropertyValue')
242 prop.Name = "nodepath"
243 prop.Value = "/org.openoffice.Setup/Product"
244 aSettings = aConfigProvider.createInstanceWithArguments("com.sun.star.configuration.ConfigurationAccess",
245 (prop,))
246 mailerstring = aSettings.getByName("ooName") + " " + \
247 aSettings.getByName("ooSetupVersion") + " via Caolan's mailmerge component"
248 except:
249 pass
251 msg['X-Mailer'] = mailerstring
252 msg['Date'] = formatdate(localtime=True)
254 for attachment in attachments:
255 content = attachment.Data
256 flavors = content.getTransferDataFlavors()
257 flavor = flavors[0]
258 ctype = flavor.MimeType
259 maintype, subtype = ctype.split('/', 1)
260 msgattachment = MIMEBase(maintype, subtype)
261 data = content.getTransferData(flavor)
262 msgattachment.set_payload(data.value)
263 encode_base64(msgattachment)
264 fname = attachment.ReadableName
265 try:
266 msgattachment.add_header('Content-Disposition', 'attachment', \
267 filename=fname)
268 except:
269 msgattachment.add_header('Content-Disposition', 'attachment', \
270 filename=('utf-8','',fname))
271 if dbg:
272 print(("PyMailSMTPService attachmentheader: ", str(msgattachment)), file=sys.stderr)
274 msg.attach(msgattachment)
276 uniquer = {}
277 for key in recipients:
278 uniquer[key] = True
279 if len(ccrecipients):
280 for key in ccrecipients:
281 uniquer[key] = True
282 if len(bccrecipients):
283 for key in bccrecipients:
284 uniquer[key] = True
285 truerecipients = uniquer.keys()
287 if dbg:
288 print(("PyMailSMTPService recipients are: ", truerecipients), file=sys.stderr)
290 self.server.sendmail(sendermail, truerecipients, msg.as_string())
292 class PyMailIMAPService(unohelper.Base, XMailService):
293 def __init__( self, ctx ):
294 self.ctx = ctx
295 self.listeners = []
296 self.supportedtypes = ('Insecure', 'Ssl')
297 self.server = None
298 self.connectioncontext = None
299 self.notify = EventObject(self)
300 if dbg:
301 print("PyMailIMAPService init", file=sys.stderr)
302 def addConnectionListener(self, xListener):
303 if dbg:
304 print("PyMailIMAPService addConnectionListener", file=sys.stderr)
305 self.listeners.append(xListener)
306 def removeConnectionListener(self, xListener):
307 if dbg:
308 print("PyMailIMAPService removeConnectionListener", file=sys.stderr)
309 self.listeners.remove(xListener)
310 def getSupportedConnectionTypes(self):
311 if dbg:
312 print("PyMailIMAPService getSupportedConnectionTypes", file=sys.stderr)
313 return self.supportedtypes
314 def connect(self, xConnectionContext, xAuthenticator):
315 if dbg:
316 print("PyMailIMAPService connect", file=sys.stderr)
318 self.connectioncontext = xConnectionContext
319 server = xConnectionContext.getValueByName("ServerName")
320 if dbg:
321 print(server, file=sys.stderr)
322 port = int(xConnectionContext.getValueByName("Port"))
323 if dbg:
324 print(port, file=sys.stderr)
325 connectiontype = xConnectionContext.getValueByName("ConnectionType")
326 if dbg:
327 print(connectiontype, file=sys.stderr)
328 tlscontext = prepareTLSContext(self, self.ctx, connectiontype.upper() == 'SSL')
329 print("BEFORE", file=sys.stderr)
330 if connectiontype.upper() == 'SSL':
331 self.server = imaplib.IMAP4_SSL(server, port, ssl_context=tlscontext)
332 else:
333 self.server = imaplib.IMAP4(server, port)
334 print("AFTER", file=sys.stderr)
336 user = xAuthenticator.getUserName()
337 password = xAuthenticator.getPassword()
338 if user != '':
339 if dbg:
340 print("Logging in, username of: " + user, file=sys.stderr)
341 self.server.login(user, password)
343 for listener in self.listeners:
344 listener.connected(self.notify)
345 def disconnect(self):
346 if dbg:
347 print("PyMailIMAPService disconnect", file=sys.stderr)
348 if self.server:
349 self.server.logout()
350 self.server = None
351 for listener in self.listeners:
352 listener.disconnected(self.notify)
353 def isConnected(self):
354 if dbg:
355 print("PyMailIMAPService isConnected", file=sys.stderr)
356 return self.server != None
357 def getCurrentConnectionContext(self):
358 if dbg:
359 print("PyMailIMAPService getCurrentConnectionContext", file=sys.stderr)
360 return self.connectioncontext
362 class PyMailPOP3Service(unohelper.Base, XMailService):
363 def __init__( self, ctx ):
364 self.ctx = ctx
365 self.listeners = []
366 self.supportedtypes = ('Insecure', 'Ssl')
367 self.server = None
368 self.connectioncontext = None
369 self.notify = EventObject(self)
370 if dbg:
371 print("PyMailPOP3Service init", file=sys.stderr)
372 def addConnectionListener(self, xListener):
373 if dbg:
374 print("PyMailPOP3Service addConnectionListener", file=sys.stderr)
375 self.listeners.append(xListener)
376 def removeConnectionListener(self, xListener):
377 if dbg:
378 print("PyMailPOP3Service removeConnectionListener", file=sys.stderr)
379 self.listeners.remove(xListener)
380 def getSupportedConnectionTypes(self):
381 if dbg:
382 print("PyMailPOP3Service getSupportedConnectionTypes", file=sys.stderr)
383 return self.supportedtypes
384 def connect(self, xConnectionContext, xAuthenticator):
385 if dbg:
386 print("PyMailPOP3Service connect", file=sys.stderr)
388 self.connectioncontext = xConnectionContext
389 server = xConnectionContext.getValueByName("ServerName")
390 if dbg:
391 print(server, file=sys.stderr)
392 port = int(xConnectionContext.getValueByName("Port"))
393 if dbg:
394 print(port, file=sys.stderr)
395 connectiontype = xConnectionContext.getValueByName("ConnectionType")
396 if dbg:
397 print(connectiontype, file=sys.stderr)
398 tlscontext = prepareTLSContext(self, self.ctx, connectiontype.upper() == 'SSL')
399 print("BEFORE", file=sys.stderr)
400 if connectiontype.upper() == 'SSL':
401 self.server = poplib.POP3_SSL(server, port, context=tlscontext)
402 else:
403 tout = xConnectionContext.getValueByName("Timeout")
404 if dbg:
405 print(isinstance(tout,int), file=sys.stderr)
406 if not isinstance(tout,int):
407 tout = _GLOBAL_DEFAULT_TIMEOUT
408 if dbg:
409 print("Timeout: " + str(tout), file=sys.stderr)
410 self.server = poplib.POP3(server, port, timeout=tout)
411 print("AFTER", file=sys.stderr)
413 user = xAuthenticator.getUserName()
414 password = xAuthenticator.getPassword()
415 if dbg:
416 print("Logging in, username of: " + user, file=sys.stderr)
417 self.server.user(user)
418 self.server.pass_(password)
420 for listener in self.listeners:
421 listener.connected(self.notify)
422 def disconnect(self):
423 if dbg:
424 print("PyMailPOP3Service disconnect", file=sys.stderr)
425 if self.server:
426 self.server.quit()
427 self.server = None
428 for listener in self.listeners:
429 listener.disconnected(self.notify)
430 def isConnected(self):
431 if dbg:
432 print("PyMailPOP3Service isConnected", file=sys.stderr)
433 return self.server != None
434 def getCurrentConnectionContext(self):
435 if dbg:
436 print("PyMailPOP3Service getCurrentConnectionContext", file=sys.stderr)
437 return self.connectioncontext
439 class PyMailServiceProvider(unohelper.Base, XMailServiceProvider, XServiceInfo):
440 def __init__( self, ctx ):
441 if dbg:
442 print("PyMailServiceProvider init", file=sys.stderr)
443 self.ctx = ctx
444 def create(self, aType):
445 if dbg:
446 print("PyMailServiceProvider create with", aType, file=sys.stderr)
447 if aType == SMTP:
448 return PyMailSMTPService(self.ctx);
449 elif aType == POP3:
450 return PyMailPOP3Service(self.ctx);
451 elif aType == IMAP:
452 return PyMailIMAPService(self.ctx);
453 else:
454 print("PyMailServiceProvider, unknown TYPE " + aType, file=sys.stderr)
456 def getImplementationName(self):
457 return g_providerImplName
459 def supportsService(self, ServiceName):
460 return g_ImplementationHelper.supportsService(g_providerImplName, ServiceName)
462 def getSupportedServiceNames(self):
463 return g_ImplementationHelper.getSupportedServiceNames(g_providerImplName)
465 class PyMailMessage(unohelper.Base, XMailMessage):
466 def __init__( self, ctx, sTo='', sFrom='', Subject='', Body=None, aMailAttachment=None ):
467 if dbg:
468 print("PyMailMessage init", file=sys.stderr)
469 self.ctx = ctx
471 self.recipients = [sTo]
472 self.ccrecipients = []
473 self.bccrecipients = []
474 self.aMailAttachments = []
475 if aMailAttachment != None:
476 self.aMailAttachments.append(aMailAttachment)
478 self.SenderName, self.SenderAddress = parseaddr(sFrom)
479 self.ReplyToAddress = sFrom
480 self.Subject = Subject
481 self.Body = Body
482 if dbg:
483 print("post PyMailMessage init", file=sys.stderr)
484 def addRecipient( self, recipient ):
485 if dbg:
486 print("PyMailMessage.addRecipient: " + recipient, file=sys.stderr)
487 self.recipients.append(recipient)
488 def addCcRecipient( self, ccrecipient ):
489 if dbg:
490 print("PyMailMessage.addCcRecipient: " + ccrecipient, file=sys.stderr)
491 self.ccrecipients.append(ccrecipient)
492 def addBccRecipient( self, bccrecipient ):
493 if dbg:
494 print("PyMailMessage.addBccRecipient: " + bccrecipient, file=sys.stderr)
495 self.bccrecipients.append(bccrecipient)
496 def getRecipients( self ):
497 if dbg:
498 print("PyMailMessage.getRecipients: " + str(self.recipients), file=sys.stderr)
499 return tuple(self.recipients)
500 def getCcRecipients( self ):
501 if dbg:
502 print("PyMailMessage.getCcRecipients: " + str(self.ccrecipients), file=sys.stderr)
503 return tuple(self.ccrecipients)
504 def getBccRecipients( self ):
505 if dbg:
506 print("PyMailMessage.getBccRecipients: " + str(self.bccrecipients), file=sys.stderr)
507 return tuple(self.bccrecipients)
508 def addAttachment( self, aMailAttachment ):
509 if dbg:
510 print("PyMailMessage.addAttachment", file=sys.stderr)
511 self.aMailAttachments.append(aMailAttachment)
512 def getAttachments( self ):
513 if dbg:
514 print("PyMailMessage.getAttachments", file=sys.stderr)
515 return tuple(self.aMailAttachments)
517 def getImplementationName(self):
518 return g_messageImplName
520 def supportsService(self, ServiceName):
521 return g_ImplementationHelper.supportsService(g_messageImplName, ServiceName)
523 def getSupportedServiceNames(self):
524 return g_ImplementationHelper.getSupportedServiceNames(g_messageImplName)
526 g_ImplementationHelper.addImplementation( \
527 PyMailServiceProvider, g_providerImplName,
528 ("com.sun.star.mail.MailServiceProvider",),)
529 g_ImplementationHelper.addImplementation( \
530 PyMailMessage, g_messageImplName,
531 ("com.sun.star.mail.MailMessage",),)
533 # vim: set shiftwidth=4 softtabstop=4 expandtab: