lenox-ai/prototype
0
1#!/usr/bin/env python32 3"""Utilities for opening files or URLs in the registered default application4and for sending e-mail using the user's preferred composer.5 6https://stackoverflow.com/a/19779373/32115067 8"""9 10__version__ = "1.1"11__all__ = ["open", "mailto"]12 13import os14import sys15import webbrowser16import subprocess17 18from email.utils import encode_rfc223119 20_controllers = {}21_open = None22 23fileopen = open24 25 26class BaseController(object):27 """Base class for open program controllers."""28 29 def __init__(self, name):30 self.name = name31 32 def open(self, filename):33 raise NotImplementedError34 35 36class Controller(BaseController):37 """Controller for a generic open program."""38 39 def __init__(self, *args):40 super(Controller, self).__init__(os.path.basename(args[0]))41 self.args = list(args)42 43 def _invoke(self, cmdline):44 if sys.platform[:3] == "win":45 closefds = False46 startupinfo = subprocess.STARTUPINFO()47 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW48 else:49 closefds = True50 startupinfo = None51 52 if (53 os.environ.get("DISPLAY")54 or sys.platform[:3] == "win"55 or sys.platform == "darwin"56 ):57 inout = fileopen(os.devnull, "r+")58 else:59 # for TTY programs, we need stdin/out60 inout = None61 62 # if possible, put the child precess in separate process group,63 # so keyboard interrupts don't affect child precess as well as64 # Python65 setsid = getattr(os, "setsid", None)66 if not setsid:67 setsid = getattr(os, "setpgrp", None)68 69 pipe = subprocess.Popen(70 cmdline,71 stdin=inout,72 stdout=inout,73 stderr=inout,74 close_fds=closefds,75 preexec_fn=setsid,76 startupinfo=startupinfo,77 )78 79 # It is assumed that this kind of tools (gnome-open, kfmclient,80 # exo-open, xdg-open and open for OSX) immediately exit after lauching81 # the specific application82 returncode = pipe.wait()83 if hasattr(self, "fixreturncode"):84 returncode = self.fixreturncode(returncode)85 return not returncode86 87 def open(self, filename):88 if isinstance(filename, str):89 cmdline = self.args + [filename]90 else:91 # assume it is a sequence92 cmdline = self.args + filename93 try:94 return self._invoke(cmdline)95 except OSError:96 return False97 98 99# Platform support for Windows100if sys.platform[:3] == "win":101 102 class Start(BaseController):103 """Controller for the win32 start progam through os.startfile."""104 105 def open(self, filename):106 try:107 os.startfile(filename)108 except WindowsError:109 # [Error 22] No application is associated with the specified110 # file for this operation: '<URL>'111 return False112 else:113 return True114 115 _controllers["windows-default"] = Start("start")116 _open = _controllers["windows-default"].open117 118 119# Platform support for MacOS120elif sys.platform == "darwin":121 _controllers["open"] = Controller("open")122 _open = _controllers["open"].open123 124# Platform support for Unix125else:126 import subprocess, stat127 128 # @WARNING: use the private API of the webbrowser module129 # from webbrowser import _iscommand130 131 def _isexecutable(cmd):132 if os.path.isfile(cmd):133 mode = os.stat(cmd)[stat.ST_MODE]134 if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:135 return True136 return False137 138 def _iscommand(cmd):139 """Return True if cmd is executable or can be found on the executable140 search path."""141 if _isexecutable(cmd):142 return True143 144 path = os.environ.get("PATH")145 if not path:146 return False147 for d in path.split(os.pathsep):148 exe = os.path.join(d, cmd)149 if _isexecutable(exe):150 return True151 return False152 153 class KfmClient(Controller):154 """Controller for the KDE kfmclient program."""155 156 def __init__(self, kfmclient="kfmclient"):157 super(KfmClient, self).__init__(kfmclient, "exec")158 self.kde_version = self.detect_kde_version()159 160 def detect_kde_version(self):161 kde_version = None162 try:163 info = subprocess.getoutput("kde-config --version")164 165 for line in info.splitlines():166 if line.startswith("KDE"):167 kde_version = line.split(":")[-1].strip()168 break169 except (OSError, RuntimeError):170 pass171 172 return kde_version173 174 def fixreturncode(self, returncode):175 if returncode is not None and self.kde_version > "3.5.4":176 return returncode177 else:178 return os.EX_OK179 180 def detect_desktop_environment():181 """Checks for known desktop environments182 183 Return the desktop environments name, lowercase (kde, gnome, xfce)184 or "generic"185 186 """187 188 desktop_environment = "generic"189 190 if os.environ.get("KDE_FULL_SESSION") == "true":191 desktop_environment = "kde"192 elif os.environ.get("GNOME_DESKTOP_SESSION_ID"):193 desktop_environment = "gnome"194 else:195 try:196 info = subprocess.getoutput("xprop -root _DT_SAVE_MODE")197 if ' = "xfce4"' in info:198 desktop_environment = "xfce"199 except (OSError, RuntimeError):200 pass201 202 return desktop_environment203 204 def register_X_controllers():205 if _iscommand("kfmclient"):206 _controllers["kde-open"] = KfmClient()207 208 for command in ("gnome-open", "exo-open", "xdg-open"):209 if _iscommand(command):210 _controllers[command] = Controller(command)211 212 def get():213 controllers_map = {214 "gnome": "gnome-open",215 "kde": "kde-open",216 "xfce": "exo-open",217 }218 219 desktop_environment = detect_desktop_environment()220 221 try:222 controller_name = controllers_map[desktop_environment]223 return _controllers[controller_name].open224 225 except KeyError:226 if "xdg-open" in _controllers:227 return _controllers["xdg-open"].open228 else:229 return webbrowser.open230 231 if os.environ.get("DISPLAY"):232 register_X_controllers()233 _open = get()234 235 236def open(filename):237 """Open a file or an URL in the registered default application."""238 239 return _open(filename)240 241 242def _fix_addersses(**kwargs):243 for headername in ("address", "to", "cc", "bcc"):244 try:245 headervalue = kwargs[headername]246 if not headervalue:247 del kwargs[headername]248 continue249 elif not isinstance(headervalue, str):250 # assume it is a sequence251 headervalue = ",".join(headervalue)252 253 except KeyError:254 pass255 except TypeError:256 raise TypeError(257 'string or sequence expected for "%s", '258 "%s found" % (headername, type(headervalue).__name__)259 )260 else:261 translation_map = {"%": "%25", "&": "%26", "?": "%3F"}262 for char, replacement in list(translation_map.items()):263 headervalue = headervalue.replace(char, replacement)264 kwargs[headername] = headervalue265 266 return kwargs267 268 269def mailto_format(**kwargs):270 # @TODO: implement utf8 option271 272 kwargs = _fix_addersses(**kwargs)273 parts = []274 for headername in ("to", "cc", "bcc", "subject", "body"):275 if headername in kwargs:276 headervalue = kwargs[headername]277 if not headervalue:278 continue279 if headername in ("address", "to", "cc", "bcc"):280 parts.append("%s=%s" % (headername, headervalue))281 else:282 headervalue = encode_rfc2231(headervalue, charset="utf-8")[283 7:284 ] # @TODO: check285 parts.append("%s=%s" % (headername, headervalue))286 287 mailto_string = "mailto:%s" % kwargs.get("address", "")288 if parts:289 mailto_string = "%s?%s" % (mailto_string, "&".join(parts))290 291 return mailto_string292 293 294def wrap_mailto(recipiant_email_summary, subject_email_summary, summary_output):295 return mailto(296 address=recipiant_email_summary,297 subject=subject_email_summary,298 body=summary_output,299 )300 301 302def mailto(address, to=None, cc=None, bcc=None, subject=None, body=None, attach=None):303 """Send an e-mail using the user's preferred composer.304 305 Open the user's preferred e-mail composer in order to send a mail to306 address(es) that must follow the syntax of RFC822. Multiple addresses307 may be provided (for address, cc and bcc parameters) as separate308 arguments.309 310 All parameters provided are used to prefill corresponding fields in311 the user's e-mail composer. The user will have the opportunity to312 change any of this information before actually sending the e-mail.313 314 address - specify the destination recipient315 cc - specify a recipient to be copied on the e-mail316 bcc - specify a recipient to be blindly copied on the e-mail317 subject - specify a subject for the e-mail318 body - specify a body for the e-mail. Since the user will be able319 to make changes before actually sending the e-mail, this320 can be used to provide the user with a template for the321 e-mail text may contain linebreaks322 attach - specify an attachment for the e-mail. file must point to323 an existing file (UNSUPPORTED)324 325 """326 327 mailto_string = mailto_format(**locals())328 return open(mailto_string)329 330 331if __name__ == "__main__":332 from optparse import OptionParser333 334 version = "%%prog %s" % __version__335 usage = (336 "\n\n%prog FILENAME [FILENAME(s)] -- for opening files"337 "\n\n%prog -m [OPTIONS] ADDRESS [ADDRESS(es)] -- for sending e-mails"338 )339 340 parser = OptionParser(usage=usage, version=version, description=__doc__)341 parser.add_option(342 "-m",343 "--mailto",344 dest="mailto_mode",345 default=False,346 action="store_true",347 help="set mailto mode. " "If not set any other option is ignored",348 )349 parser.add_option(350 "--cc", dest="cc", help="specify a recipient to be " "copied on the e-mail"351 )352 parser.add_option(353 "--bcc",354 dest="bcc",355 help="specify a recipient to be " "blindly copied on the e-mail",356 )357 parser.add_option(358 "--subject", dest="subject", help="specify a subject for the e-mail"359 )360 parser.add_option(361 "--body",362 dest="body",363 help="specify a body for the "364 "e-mail. Since the user will be able to make changes "365 "before actually sending the e-mail, this can be used "366 "to provide the user with a template for the e-mail "367 "text may contain linebreaks",368 )369 parser.add_option(370 "--attach",371 dest="attach",372 help="specify an attachment "373 "for the e-mail. file must point to an existing file",374 )375 # breakpoint()376 (options, args) = parser.parse_args()377 378 if not args:379 parser.print_usage()380 parser.exit(1)381 382 if options.mailto_mode:383 if not mailto(384 args,385 None,386 options.cc,387 options.bcc,388 options.subject,389 options.body,390 options.attach,391 ):392 sys.exit("Unable to open the e-mail client")393 else:394 for name in ("cc", "bcc", "subject", "body", "attach"):395 if getattr(options, name):396 parser.error(397 'The "cc", "bcc", "subject", "body" and "attach" '398 "options are only accepten in mailto mode"399 )400 success = False401 for arg in args:402 if not open(arg):403 print('Unable to open "%s"' % arg)404 else:405 success = True406 sys.exit(success)407 