1 #----------------------------------------------------------------------------
2 # Name: ErrorDialogs.py
4 # Created: September--October 2001
5 # Author: Chris Fama of Wholly Snakes Software,
6 # Chris.Fama@whollysnakes.com
7 #----------------------------------------------------------------------------
9 ErrorDialogs.py: by Christopher J. Fama (trading under the name
10 Wholly Snakes Software {Australian Business Number: 28379514278}).
12 This code is released under the auspices of the modified version of
13 the GNU Library Public License (LGPL) that constitutes the license
14 under which the GUI package wxPython is released [see the file
15 LICENSE.TXT accompanying that distribution). I must also thank Graham
16 Boyd, of Boyd Mining, and CEO of the Minserve group of companies
17 (www.minserve.com.au), for kindly allowing the release of this
18 module under a "free" license, despite a certain part of it's
19 development having taken place while working for him...
21 Please note that this code, written for Python 2.1, is derives from
22 code written when Python 1.5.2 was current. Although since Python 2.0
23 the sys.excepthook variable has been available, for ease and potential
24 backwards compatibility (or to be more realistic
25 backwards-PARTIAL-compatibility!), the code catches errors by
26 assigning a custom object to sys.stderr and monitoring calls to it's
27 write() method. Such calls which take place with a new value of
28 sys.last_traceback stand as evidence that at interpreter error has
29 just occurred; please note that this means that NO OTHER OUTPUT TO
30 sys.stderr WILL BE DISPLAYED. THIS INCLUDES "warnings" generated by
31 the interpreter. As far as I am aware, unless your code itself writes
32 to sys.stderr itself, these will be the only things you miss out
33 on--and many, if not most or all, of these will occur before your code
34 even gets the opportunity to set one of these file-like objects in
35 place. If this is a problem for you and you can't work around it,
36 please contact means or the wxPython-users mailing list.
41 This is a module to display errors--either explicitly requested by a
42 script or arbitrary interpreter errors--these needs arose in the
43 course of a commercial (contract) project, but were not developed in
44 the course of work on such [well, very little, albeit concurrently]...
45 [NBNB.Does not currently support display of other than interpreter errors.]
47 Usage, after 'from wxPython.lib.ErrorDialogs import *' (all
48 identifiers defined in this module begin with "wxPy", and many of them
49 with "wxPyError_", so there should be no namespace conflicts...):
51 wxPyNewErrorDialog (<win> (['frame='] <frame (can be None)>,
54 wxPyDestroyErrorDialogIfPresent () # e.g. when top frame destroyed)
56 for unhandled errors, or
58 returnval = wxpyNonFatalError (<frame (can be None)>,
61 [NOTE: NOT IMPLEMENTED {IN THIS VERSION} YET...]
63 returnval = wxPyFatalError (<HTML message>,
64 [,<OPTIONS, an extra one of which may be
65 'frame=' <frame (defaults to None)>>])
66 [NOTE: NOT IMPLEMENTED {IN THIS VERSION} YET...]
69 wxPybNonWindowingError (message)
74 wxPyNonFatalErrorDialog
76 wxPyFatalErrorDialogWithTraceback
77 wxPyNonFatalErrorDialogWithTraceback
78 wxPyNonWindowingErrorHandler
80 and the OPTIONS (with defaults) are: (please note that the options for
81 wxPyNonWindowingErrorHandler / wxPyNonWindowingError are 'almost' a (small))
84 'modal' [default 1]: block until dismissed.
86 'programname' [default "Python program"]: appears inThe
87 caption of the dialog, amidst it's text and (by default) in mailings.
89 'whendismissed' option, if given, this should be a string, to be
90 executed in a restricted environment (see rexec module) after
91 dialog dismissal. Typically, this should be Python code to "clean
92 up" after what was presumably the partial execution of some
93 operation started by the user, after the unexpected interruption
94 by some error [which--at least the way I work, means an unexpected
95 error in the code, since exceptions that may be raised by some
96 foreseen (in a programmatic sense) should normally be handled by
97 try...except clauses].
98 NOTE THAT CURRENTLY THE rexec CODE IS NOT WORKING, SO THIS IS JUST DONE
101 'mailto': if None, give no "e-mail support" option, otherwise this
102 is meant to be an address for 'bug reports'; a command button will
103 be created for this, which pops up another dialog [Linux], or a window of
104 another program [Windows].
106 On Windows, this will launch your mailer (assuming your mailer
107 would start when a file with the '.eml'extension is double-clicked
108 in Windows Explorer--this will be the case for Microsoft Outlook
109 Express [tm and all those legal necessities] if installed, and the
110 association has not been taken over by some other program. On
111 Linux, it will open a file dialog to save the message, by default
112 as a '.html' file...{Please note that, on Windows and with current
113 (when I got the machine I'm writing this on--early 2000) versions
114 of Outlook Express, you may need to enter your e-mail address in
115 the box beside the "mail support" button.)
117 The template for the mail message is in the 'MessageTemplate'
118 attribute of the dialog-derived class in question (e.g.,
119 wxPyNonFatalErrorDialog). Search for this in the code below to
120 see the default (note that this template is in HTML format). This
121 attributes uses the '%{name}s' string conversion feature of Python
122 [see sec.2 of the Python Library Reference]; allowed Lance are:
123 programname version exceptionname exceptionvalue
124 extraexceptioninformation traceback
126 'version': str(this) will appear after 'Version' below "Error in
127 <programname>" at the top of the dialog.
131 sys.stderr = wxPyNonFatalErrorWindowWithTraceback (
133 programname='sumthing',
135 whendismissed="from wxPython.wx import * ; wxBell()")
137 FOR INTERNATIONAL [NON-ENGLISH-SPEAKING] USE:
138 wxPyNonFatalErrorDialog and relatives have the method
139 SetText(Number NUMBER, STRING)
141 where STRING is to displayed in the wxStaticText control with ID
142 wxPyError_ID_TEXT<NUMBER>--see the automatically-generated code
143 for information about the meaning of these...
148 #_debug = 1 # uncomment to display some information (to stdout)
150 from wxPython
.wx
import *
151 import string
, sys
, traceback
, time
, rexec
, operator
, types
, tempfile
, os
152 #from wxPython.lib.createandsendHTMLmail import *# now inline
153 import MimeWriter
, mimetools
, cStringIO
, smtplib
155 from ErrorDialogs_wdr
import *
157 # You may see from the above line that I used the excellent RAD tool
158 # wxDesigner, by Robert Roebling, to accelerate development of this
159 # module... The above is left for the convenience of making future
160 # changes with wxDesigner; also so the wxDesigner-generated codedoes
161 # not need to precede the "hand-generated" code in this file; finally,
162 # as a personal endorsement: it is truly a brilliant time-saver!
163 # Please note that, at the time of writing, the wxDesigner-generated
164 # output requires manual removal of the PythonBitmaps function--an
165 # appropriate version of this function will be imported from a
166 # similarly- named module. Another manual change will have to be made
167 # to the automatically-generated source: "parent.sizerAroundText = "
168 # should be added [immediately] before the text similar to "item13 =
169 # wxStaticBoxSizer( item14, wxVERTICAL )", this sizer being the one
170 # containing the wxTextCtrl... [IMPORTANT NOTE: THIS LINE SHOULD BE
171 # THE ONE INSTANTIATING A wxStaticBoxSizer, *NOT* THE wxStaticBox
174 def wxPyDestroyErrorDialogIfPresent():
175 if isinstance(sys
.stderr
,wxPyNonFatalErrorDialog
):
179 def wxPyNewErrorDialog(dlg
):
180 wxPyDestroyErrorDialogIfPresent()
183 class wxPyNonWindowingErrorHandler
:
186 def __init__(self
,fatal
=0,file=sys
.__stderr
__):
191 if string
.find(s
,"Warning") <> 0\
192 and self
.this_exception
is not sys
.last_traceback
:
193 wxPyNonWindowingError("The Python interpreter encountered an error "
194 "not handled by any\nexception handler--this "
195 "may represent some programming error.",
199 self
.this_exception
= sys
.last_traceback
201 def wxPyNonWindowingError(msg
,#output=None,errors=None,
202 stderr
=sys
.__stderr
__,
205 if os
.path
.exists("wxPyNonWindowingErrors.txt"):
209 fl
= open("wxPyNonWindowingErrors.txt",mode
)
210 if stderr
is not None:
211 l
= [fl
,stderr
] # so that the error will be written to the file
212 # before any potential error in stderr.write()... (this is largely
213 # for my own sake in developing...)
217 f
.write(time
.ctime (time
.time ()) + ": ")
220 if sys
.exc_info () [0] is not None:
222 f
.write('Currently handled exception:\n')
224 traceback
.print_exc(file=f
)
226 f
.write('\nPrevious (?) error:\n')
227 elif last
or sys
.last_traceback
:
228 f
.write("\n\n(For wizards only) ")
230 if type(last
) <> types
.ListType
or len(last
) < 3:
231 if (hasattr(sys
,"last_traceback") and sys
.last_traceback
):
232 last
= [sys
.last_type
,sys
.last_value
,sys
.last_traceback
]
233 if type(last
) == types
.ListType
:
234 traceback
.print_exception(last
[0],last
[1],last
[2],
237 if f
is sys
.__stderr
__:
238 s
= ' (see the file "wxPyNonWindowingErrors.txt")'
241 f
.write("Please contact the author with a copy of this\n"
245 if fatal
and stderr
is sys
.__stderr
__:
246 if sys
.platform
in ["windows",'nt',"win32"]:
247 sys
.__stderr
__.write(
248 "\nYou may have to manually close this window to exit.")
251 class wxPythonRExec (rexec
.RExec
):
252 def __init__(self
,securityhole
=0,*args
,**kwargs
):
253 apply(rexec
.RExec
.__init
__, (self
,) + args
, kwargs
)
255 self
.ok_builtin_modules
= self
.ok_builtin_modules
+ \
256 ('wxPython', 'wxPython.wxc','wxPython.wx','wxPython.misc',
257 'wxPython.misc2', 'wxPython.windows', 'wxPython.gdi',
258 'wxPython.clip_dnd', 'wxPython.events', 'wxPython.mdi',
259 'wxPython.frames', 'wxPython.stattool', 'wxPython.controls',
260 'wxPython.controls2', 'wxPython.windows2', 'wxPython.cmndlgs',
261 'wxPython.windows3', 'wxPython.image', 'wxPython.printfw',
262 'wxc','misc', 'misc2', 'windows', 'gdi', 'clip_dnd', 'events',
263 'mdi', 'frames', 'stattool', 'controls', 'controls2', 'windows2',
264 'cmndlgs', 'windows3', 'image', 'printfw', 'wx')
265 # possible security hole!
267 ##def wxPyFatalError(msg,frame=None,**kwargs):
268 ## kwargs.update({'fatal' : 1})
269 ## apply(wxPyNonFatalError,
273 class wxPyNonFatalErrorDialogWithTraceback(wxDialog
):
275 populate_function
= populate_wxNonFatalErrorDialogWithTraceback
276 no_continue_button
= false
279 exitjustreturns
= false
# really only for testing!
281 def __init__(self
, parent
, id,
282 pos
= wxPyDefaultPosition
, size
= wxPyDefaultSize
,
283 style
= wxDEFAULT_DIALOG_STYLE
,
284 programname
= "Python program",
288 disable_exit_button
= false
):
291 sys
.stdout
.write('\nwxPyNonFatalErrorWindow.__init__: '
298 title
= "A (%sfatal) error has occurred in %s!"\
299 % (whetherNF
,programname
)
300 self
.programname
= programname
# save for later use
301 self
.mailto
= mailto
# save for later use
302 self
.parent
= parent
# save for later use
303 self
.whendismissed
= whendismissed
# save for later use
304 self
.dialogtitle
= title
# save for later use
306 wxDialog
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
308 self
.topsizer
= self
.populate_function( false
,true
)
310 self
.SetProgramName(programname
)
311 self
.SetVersion(version
)
313 if not self
.no_continue_button
:
314 EVT_BUTTON(self
, wxPyError_ID_CONTINUE
, self
.OnContinue
)
316 disable_mail_button
= 0
318 disable_mail_button
= 1
319 if not disable_mail_button
:
320 EVT_BUTTON(self
, wxPyError_ID_MAIL
, self
.OnMail
)
322 self
.GetMailButton().Enable(false
)
323 if not disable_exit_button
:
324 EVT_BUTTON(self
, wxPyError_ID_EXIT
, self
.OnExit
)
326 self
.nonwindowingerror
= wxPyNonWindowingErrorHandler(file=sys
.__stderr
__,
330 sys
.stdout
.write('\nwxPyNonFatalErrorWindow.__init__: '
332 def GetExtraInformation(self
):
333 return self
.extraexceptioninformation
335 def SetExtraInformation(self
,value
):
336 self
.extraexceptioninformation
= value
337 c
= self
.GetExtraInformationCtrl()
339 c
.SetLabel(str(value
))
340 self
.topsizer
.Layout()
342 def GetExtraInformationCtrl(self
):
343 return self
.FindWindowById(wxPyError_ID_EXTRAINFORMATION
)
345 def GetExceptionName(self
):
346 return str(self
.exceptiontype
)
348 def SetExceptionName(self
,value
):
349 self
.exceptiontype
= str(value
)
350 c
= self
.GetExceptionNameCtrl()
352 c
.SetLabel(str(value
))
353 self
.topsizer
.Layout()
355 def GetExceptionNameCtrl(self
):
356 return self
.FindWindowById(wxPyError_ID_EXCEPTIONNAME
)
358 def GetTraceback(self
):
360 return self
.traceback
361 except AttributeError:
364 def SetTraceback(self
,value
):
365 self
.traceback
= value
366 c
= self
.GetTracebackCtrl()
368 s
,cs
= c
.GetSize(), c
.GetClientSize()
369 if value
[-1] == '\n':
372 print "%s.SetTraceback(): ...SetValue('%s' (^M=\\r; ^J=\\n))"\
373 % (self
,string
.replace(value
,'\n',"^J"))
376 # Despite using the wxADJUST_MINSIZE flag in the
377 # appropriate AddWindow method of the sizer, this doesn't
378 # size the control appropriately... evidently the control's
379 # GetBestSize method is not returning the "correct"
380 # value... So we perform a rather ugly "fix"... note that
381 # this also requires that we remove the wxADJUST_MINSIZE
382 # flag from the AddWindow method of the sizer containing
383 # the wxTextCtrl, which adds the wxTextCtrl... (this
384 # amounts, as of wxDesigner 2.6, to only a few mouse
388 size
= c
.GetBestSize()
389 print "%s.SetTraceback(): %s.GetBestSize() = (%s,%s)"\
390 % (self
,c
,size
.width
,size
.height
)
392 for v
in string
.split(value
,"\n"):
393 pw
,ph
,d
,e
= t
= c
.GetFullTextExtent(v
)
396 h
= h
+ ph
+ e
# + d# + e
397 pw
= pw
+ wxSystemSettings_GetSystemMetric(wxSYS_VSCROLL_X
)
400 w
= w
+ s
.width
- cs
.width
401 h
= h
+ s
.height
- cs
.height
403 print "%s.SetTraceback(): calculated w,h =" % c
,\
404 w
,h
,"and sys.platform = '%s'" % sys
.platform
405 self
.sizerAroundText
.SetItemMinSize (c
,w
,h
)
407 c
.SetSizeHints (w
,h
,w
,h
)
408 c
.Refresh()#.SetAutoLayout(FALSE)
410 #^ the reason we need the above seems to be to replace the
411 #faulty GetBestSize of wxTextCtrl...
412 #self.sizerAroundText.Layout()
413 self
.topsizer
.Layout()
415 def GetTracebackCtrl(self
):
416 return self
.FindWindowById(wxPyError_ID_TEXTCTRL
)
418 def GetVersion(self
):
421 def SetVersion(self
,value
):
423 c
= self
.GetVersionNumberCtrl()
426 self
.topsizer
.Layout()
428 def GetVersionNumberCtrl(self
):
429 return self
.FindWindowById(wxPyError_ID_VERSIONNUMBER
)
431 def GetProgramName(self
):
432 return self
.programname
434 def SetProgramName(self
,value
):
435 self
.programname
= value
436 c
= self
.GetProgramNameCtrl()
439 self
.topsizer
.Layout()
441 def GetProgramNameCtrl(self
):
442 return self
.FindWindowById(wxPyError_ID_PROGRAMNAME
)
444 def GetContinueButton(self
):
445 return self
.FindWindowById(wxPyError_ID_CONTINUE
)
447 def GetMailButton(self
):
448 return self
.FindWindowById(wxPyError_ID_MAIL
)
450 def GetExitButton(self
):
451 return self
.FindWindowById(wxPyError_ID_EXIT
)
453 # write handler (which is really the guts of the thing...
454 # [Note that this doesn't use sys.excepthook because I already had a
455 # working body of code...
458 if self
.this_exception
is not sys
.last_traceback
:
459 if not wxThread_IsMain():
460 # Aquire the GUI mutex before making GUI calls. Mutex is released
461 # when locker is deleted at the end of this function.
462 locker
= wxMutexGuiLocker()
464 self
.this_exception
= sys
.last_traceback
465 # this is meant to be done once per traceback's sys.stderr.write's
466 # - on the first in fact.....
468 #from wxPython.wx import wxBell
473 'in %s.write(): ' % self
)
475 self
.exceptiontype
= sys
.last_type
476 self
.extraexceptioninformation
= sys
.last_value
477 c
= cStringIO
.StringIO()
478 traceback
.print_last(None,c
)
479 self
.traceback
= c
.getvalue()
483 traceback
.print_last(None,sys
.stdout
)
485 self
.SetExceptionName(str(self
.exceptiontype
))
486 self
.SetExtraInformation(str(self
.extraexceptioninformation
))
487 self
.SetTraceback(str(self
.traceback
))
489 self
.topsizer
.Fit(self
)
490 self
.topsizer
.SetSizeHints(self
)
498 if not locals().has_key("c"):
499 c
= cStringIO
.StringIO()
500 c
.write("[Exception occurred before data from "
501 "sys.last_traceback available]")
502 ## c2 = cStringIO.StringIO()
503 ## traceback.print_exception(None,c2)
504 wxPyNonWindowingError("Warning: "
505 "a %s error was encountered trying to "
506 "handle the exception\n%s\nThis was:"#%s\n"
507 % (sys
.exc_type
, c
.getvalue()),#, c2.getvalue()),
508 stderr
=sys
.__stderr
__,
514 def OnContinue(self
, event
):
516 if self
.whendismissed
:
517 parent
= self
.parent
# so whendismissed can refer to "parent"
520 sys
.stdout
.write("exec '''%s''': "
521 % (self
.whendismissed
))
522 exec self
.whendismissed
523 if _debug
: print "\n",
526 sys
.stdout
.write("wxPythonRExec(%s).r_exec('''%s'''): "
527 % (self
.securityhole
,
529 wxPythonRExec(self
.securityhole
).r_exec(self
.whendismissed
)
530 if _debug
: print "\n",
532 self
.EndModal(wxID_OK
)
535 if _debug
: print "reimporting ",
536 for m
in sys
.modules
.values():
537 if m
and m
.__dict
__["__name__"][0] in string
.uppercase
:#hack!
539 print m
.__dict
__["__name__"],
544 print '\nENDING %s.OnContinue()..\n\n' % (self
,),
546 wxPyNonWindowingError("Warning: the following exception information"
547 " may not be the full story.. (because "
548 "a %s(%s) error was encountered trying to "
549 "handle the exception)\n\n"
550 % tuple(sys
.exc_info()[:2]),
551 stderr
=sys
.__stderr
__,
554 MessageTemplate
= "<head>"\
556 '<body text="#000000" bgcolor="#FFFFFF">'\
558 "<i><b>Hello,</b></i>\n<p>\n"\
559 '<p><h2><font color="#CC6C00">%(programname)s</font>'\
561 "I encountered the following error when running your "\
562 'program <font color="#CC6C00">%(programname)s</font>,'\
563 "at %(date)s.\n<p>\n"\
565 "<h2>Traceback (automatically generated):</h2>\n"\
566 '<p><font size="-1">\n<pre>%(traceback)s</pre>\n<p></font><p>'\
567 "\n<p>\n<h2>More information follows:</h2>\n<p>\n"\
568 '<font color="#CC6C00">'\
570 "information about the error here, such as what you were "\
571 "trying to do at the time of the error. Please "\
572 "understand that failure to fill in this field will be "\
573 "interpreted as an invitation to consign this e-mail "\
574 "straight to the trash can!]\n</i><p>\n"\
576 '<i><b>Yours sincerely,</b></i>\n<p>'\
577 '<font color="#CC6C00">'\
578 "[insert your name here]\n"\
582 def OnMail(self
,event
):
585 print 'Attempting to write mail message.\n',
586 gmtdate
= time
.asctime(time
.gmtime(time
.time())) + ' GMT'
587 tm
= time
.localtime(time
.time())
588 date
= time
.asctime(tm
) + ' ' +\
589 time
.strftime("%Z",tm
)
590 programname
= self
.programname
591 traceback
= self
.traceback
593 message
= self
.MessageTemplate
% vars()
594 subject
= "Un-caught exception when running %s." % programname
596 print 'message:',message
597 print 'subject:,',subject
598 print 'sent to:',mailto
599 mailfrom
= self
.FindWindowById (wxPyError_ID_ADDRESS
)
601 mailfrom
= mailfrom
.GetValue()
602 if _startmailerwithhtml(mailto
,subject
,message
,text
="",mailfrom
=mailfrom
):
603 if not (hasattr(self
,"fatal") and self
.fatal
):
604 self
.OnContinue(event
) # if ok, then act as if "Continue" selected
606 wxPyNonWindowingError("Warning: the following exception information"
607 " may not be the full story... (because "
608 "a %s error was encountered trying to "
609 "handle the original exception)\n\n"#%s"
610 % (sys
.exc_type
,),#self.msg),
611 stderr
=sys
.__stderr
__,
614 def OnExit(self
, event
):
616 self
.EndModal(wxID_CANCEL
)
617 if self
.exitjustreturns
:
619 wxGetApp().ExitMainLoop()
621 ## if isinstance(sys.stderr,wxPyNonFatalErrorDialogWithTraceback):
622 ## if sys.stderr == self:
624 ## sys.stderr.Destroy()
625 ## sys.stderr = wxPyNonWindowingErrorHandler(sys.__stderr__)
626 ## wxSafeYield() # so remaining events are processed...
627 ## if self.parent not in [None,NULL]:
628 ## self.parent.Destroy()
629 ## elif "selfdestroyed" not in locals().keys():
632 def SetText(self
,number
,string
):
633 self
.FindWindowById(eval("wxPyError_ID_TEXT%d"
634 % number
)).SetLabel(string
)
635 self
.topsizer
.Layout()
637 class wxPyFatalErrorDialogWithTraceback(wxPyNonFatalErrorDialogWithTraceback
):
638 populate_function
= populate_wxFatalErrorDialogWithTraceback
639 no_continue_button
= true
642 class wxPyNonFatalErrorDialog(wxPyNonFatalErrorDialogWithTraceback
):
643 populate_function
= populate_wxNonFatalErrorDialog
645 class wxPyFatalErrorDialog(wxPyFatalErrorDialogWithTraceback
):
646 populate_function
= populate_wxFatalErrorDialog
649 def _startmailerwithhtml(mailto
,subject
,html
,text
,mailfrom
=None):
650 if sys
.platform
in ["windows",'nt',"win32"] and sys
.hexversion
>= 0x02000000:
651 name
= tempfile
.mktemp(".eml")
653 f
.write(_createhtmlmail(html
,text
,subject
,to
=mailto
,
659 # probably no association with eml files
660 return _writehtmlmessage(mailto
,subject
,html
,text
,mailfrom
=mailfrom
)
663 return _writehtmlmessage(mailto
,subject
,html
,text
,mailfrom
=mailfrom
)
665 def _writehtmlmessage(mailto
,subject
,html
,text
=None,parent
=None,mailfrom
=None):
666 dlg
= wxFileDialog (parent
,
667 "Please choose a a file to save the message to...",
670 "HTML files (*.htm,*.html)|*.htm,*.html|"
672 wxSAVE | wxHIDE_READONLY
)
673 if dlg
.ShowModal() <> wxID_CANCEL
:
674 f
= open(dlg
.GetPath(),"w")
676 f
.write(_createhtmlmail(html
,text
,subject
,to
=mailto
,mailfrom
=mailfrom
))
682 # PLEASE NOTE THAT THE CODE BELOW FOR STARTING MAILER WITH A GIVEN
683 #(HTML) MESSAGE IS BY ART GILLESPIE [with slight modifications by yours truly].
685 def _createhtmlmail (html
, text
, subject
, to
=None, mailfrom
=None):
686 """Create a mime-message that will render HTML in popular
687 MUAs, text in better ones (if indeed text is not untrue (e.g. None)
689 # imported above #import MimeWriter, mimetools, cStringIO
691 out
= cStringIO
.StringIO() # output buffer for our message
692 htmlin
= cStringIO
.StringIO(html
)
694 txtin
= cStringIO
.StringIO(text
)
696 writer
= MimeWriter
.MimeWriter(out
)
698 # set up some basic headers... we put subject here
699 # because smtplib.sendmail expects it to be in the
703 writer
.addheader("From", mailfrom
)
704 #writer.addheader("Reply-to", mailfrom)
705 writer
.addheader("Subject", subject
)
707 writer
.addheader("To", to
)
708 writer
.addheader("MIME-Version", "1.0")
710 # start the multipart section of the message
711 # multipart/alternative seems to work better
712 # on some MUAs than multipart/mixed
714 writer
.startmultipartbody("alternative")
715 writer
.flushheaders()
717 # the plain text section
720 subpart
= writer
.nextpart()
721 subpart
.addheader("Content-Transfer-Encoding", "quoted-printable")
722 pout
= subpart
.startbody("text/plain", [("charset", 'us-ascii')])
723 mimetools
.encode(txtin
, pout
, 'quoted-printable')
726 # start the html subpart of the message
728 subpart
= writer
.nextpart()
729 subpart
.addheader("Content-Transfer-Encoding", "quoted-printable")
730 pout
= subpart
.startbody("text/html", [("charset", 'us-ascii')])
731 mimetools
.encode(htmlin
, pout
, 'quoted-printable')
734 # Now that we're done, close our writer and
735 # return the message body
742 def _sendmail(mailto
,subject
,html
,text
):# currently unused
743 """For illustration only--this function is not actually used."""
744 message
= _createhtmlmail(html
, text
, subject
)
745 server
= smtplib
.SMTP("localhost")
746 server
.sendmail(mailto
, subject
, message
)