diff --git a/setup.py b/setup.py index 427247526a..f446b34222 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,6 @@ if __name__ == '__main__': author='Kovid Goyal', author_email='kovid@kovidgoyal.net', url = 'http://%s.kovidgoyal.net'%APPNAME, - include_package_data = True, package_data = {'': ['*.so.*', '*.so']}, entry_points = entry_points, zip_safe = False, diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index a63e0a1904..1d26f3667d 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -550,10 +550,12 @@ if islinux: sys.path.insert(1, plugins) cwd = os.getcwd() os.chdir(plugins) - try: - import pictureflow - except: - import traceback - traceback.print_exc() - pictureflow = None +try: + import pictureflow +except: + import traceback + traceback.print_exc() + pictureflow = None + +if islinux: os.chdir(cwd) diff --git a/src/calibre/ebooks/lrf/web/__init__.py b/src/calibre/ebooks/lrf/web/__init__.py index 6fae0f5262..30609f014e 100644 --- a/src/calibre/ebooks/lrf/web/__init__.py +++ b/src/calibre/ebooks/lrf/web/__init__.py @@ -12,7 +12,6 @@ from calibre.ebooks.lrf.web.profiles.faznet import FazNet from calibre.ebooks.lrf.web.profiles.wsj import WallStreetJournal from calibre.ebooks.lrf.web.profiles.barrons import Barrons from calibre.ebooks.lrf.web.profiles.portfolio import Portfolio -from calibre.ebooks.lrf.web.profiles.dilbert import Dilbert from calibre.ebooks.lrf.web.profiles.cnn import CNN from calibre.ebooks.lrf.web.profiles.chr_mon import ChristianScienceMonitor from calibre.ebooks.lrf.web.profiles.jpost import JerusalemPost @@ -28,7 +27,7 @@ from calibre.ebooks.lrf.web.profiles.nasa import NASA builtin_profiles = [Atlantic, AssociatedPress, Barrons, BBC, - ChristianScienceMonitor, CNN, Dilbert, Economist, FazNet, + ChristianScienceMonitor, CNN, Economist, FazNet, JerusalemPost, Jutarnji, NASA, Newsweek, NewYorker, NewYorkReviewOfBooks, NYTimes, UnitedPressInternational, USAToday, Portfolio, Reuters, SpiegelOnline, WallStreetJournal, diff --git a/src/calibre/gui2/dialogs/user_profiles.py b/src/calibre/gui2/dialogs/user_profiles.py index 69b35949f9..aabc2d6ca1 100644 --- a/src/calibre/gui2/dialogs/user_profiles.py +++ b/src/calibre/gui2/dialogs/user_profiles.py @@ -1,6 +1,7 @@ +from calibre.gui2 import choose_files __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import time +import time, os from PyQt4.QtCore import SIGNAL from PyQt4.QtGui import QDialog, QMessageBox @@ -9,7 +10,9 @@ from calibre.web.feeds.recipes import compile_recipe from calibre.web.feeds.news import AutomaticNewsRecipe from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog from calibre.gui2 import qstring_to_unicode, error_dialog, question_dialog -from calibre.gui2.widgets import PythonHighlighter +from calibre.gui2.widgets import PythonHighlighter +from calibre.utils import sendmail +from calibre.ptempfile import PersistentTemporaryFile class UserProfiles(QDialog, Ui_Dialog): @@ -24,6 +27,10 @@ class UserProfiles(QDialog, Ui_Dialog): self.available_profiles.remove_selected_items) self.connect(self.add_feed_button, SIGNAL('clicked(bool)'), self.add_feed) + self.connect(self.load_button, SIGNAL('clicked()'), self.load) + self.connect(self.share_button, SIGNAL('clicked()'), self.share) + self.connect(self.down_button, SIGNAL('clicked()'), self.down) + self.connect(self.up_button, SIGNAL('clicked()'), self.up) self.connect(self.add_profile_button, SIGNAL('clicked(bool)'), self.add_profile) self.connect(self.feed_url, SIGNAL('returnPressed()'), self.add_feed) @@ -36,7 +43,36 @@ class UserProfiles(QDialog, Ui_Dialog): for title, src in feeds: self.available_profiles.add_item(title, (title, src), replace=True) + def up(self): + row = self.added_feeds.currentRow() + item = self.added_feeds.takeItem(row) + if item is not None: + self.added_feeds.insertItem(max(row-1, 0), item) + self.added_feeds.setCurrentItem(item) + + def down(self): + row = self.added_feeds.currentRow() + item = self.added_feeds.takeItem(row) + if item is not None: + self.added_feeds.insertItem(row+1, item) + self.added_feeds.setCurrentItem(item) + + def share(self): + row = self.available_profiles.currentRow() + item = self.available_profiles.item(row) + if item is None: + error_dialog(self, _('No recipe selected'), _('No recipe selected')).exec_() + return + title, src = item.user_data + pt = PersistentTemporaryFile(suffix='.py') + pt.write(src.encode('utf-8')) + pt.close() + sendmail(subject='Recipe for '+title, + attachments=[pt.name], + body='The attached file: %s is a recipe to download %s.'%(os.path.basename(pt.name), title)) + + def edit_profile(self, current, previous): if not current: current = previous @@ -145,6 +181,28 @@ class %(classname)s(%(base_class)s): return self.clear() + def load(self): + files = choose_files(self, 'recipe loader dialog', _('Choose a recipe file'), filters=[(_('Recipes'), '*.py')], all_files=False, select_only_single_file=True) + if files: + file = files[0] + src = open(file, 'rb').read().decode('utf-8') + try: + title = compile_recipe(src).title + except Exception, err: + error_dialog(self, _('Invalid input'), + _('

Could not create recipe. Error:
%s')%str(err)).exec_() + return + try: + self.available_profiles.add_item(title, (title, src), replace=False) + except ValueError: + d = question_dialog(self, _('Replace recipe?'), + _('A custom recipe named %s already exists. Do you want to replace it?')%title) + if d.exec_() == QMessageBox.Yes: + self.available_profiles.add_item(title, (title, src), replace=True) + else: + return + self.clear() + def populate_options(self, profile): self.oldest_article.setValue(profile.oldest_article) self.max_articles.setValue(profile.max_articles_per_feed) diff --git a/src/calibre/gui2/dialogs/user_profiles.ui b/src/calibre/gui2/dialogs/user_profiles.ui index 3895950c03..b41c9c0165 100644 --- a/src/calibre/gui2/dialogs/user_profiles.ui +++ b/src/calibre/gui2/dialogs/user_profiles.ui @@ -78,6 +78,26 @@ + + + + &Share recipe + + + :/images/forward.svg + + + + + + + &Load recipe from file + + + :/images/chapters.svg + + + @@ -205,17 +225,41 @@ p, li { white-space: pre-wrap; } - - - Remove feed from recipe - - - ... - - - :/images/list_remove.svg - - + + + + + ... + + + :/images/arrow-up.svg + + + + + + + Remove feed from recipe + + + ... + + + :/images/list_remove.svg + + + + + + + ... + + + :/images/arrow-down.svg + + + + diff --git a/src/calibre/utils/__init__.py b/src/calibre/utils/__init__.py new file mode 100644 index 0000000000..d1ea35c192 --- /dev/null +++ b/src/calibre/utils/__init__.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Miscelleaneous utilities. +''' +import subprocess + +from calibre import iswindows, isosx +from calibre.ptempfile import PersistentTemporaryFile + +def sendmail(recipient='', subject='', attachments=[], body=''): + if not recipient: + recipient = 'someone@somewhere.net' + if isinstance(recipient, unicode): + recipient = recipient.encode('utf-8') + if isinstance(subject, unicode): + subject = subject.encode('utf-8') + if isinstance(body, unicode): + body = body.encode('utf-8') + for i, src in enumerate(attachments): + if isinstance(src, unicode): + attachments[i] = src.encode('utf-8') + + if iswindows: + from calibre.utils.simplemapi import SendMail + SendMail(recipient, subject=subject, body=body, attachfiles=';'.join(attachments)) + elif isosx: + pt = PersistentTemporaryFile(suffix='.txt') + pt.write(body) + if attachments: + pt.write('\n\n----\n\n') + pt.write(open(attachments[0], 'rb').read()) + pt.close() + + subprocess.check_call('open -t '+pt.name, shell=True) + else: + body = '"' + body.replace('"', '\\"') + '"' + subject = '"' + subject.replace('"', '\\"') + '"' + attach = '' + if attachments: + attach = attachments[0] + attach = '"' + attach.replace('"', '\\"') + '"' + attach = '--attach '+attach + subprocess.check_call('xdg-email --utf8 --subject %s --body %s %s %s'%(subject, body, attach, recipient), shell=True) \ No newline at end of file diff --git a/src/calibre/utils/simplemapi.py b/src/calibre/utils/simplemapi.py new file mode 100644 index 0000000000..453ca29db3 --- /dev/null +++ b/src/calibre/utils/simplemapi.py @@ -0,0 +1,262 @@ +""" +Copyright (c) 2007 Ian Cook and John Popplewell + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Date : 13 August 2007 +Version : 1.0.2 +Contact : John Popplewell +Email : john@johnnypops.demon.co.uk +Web : http://www.johnnypops.demon.co.uk/python/ +Origin : Based on the original script by Ian Cook + http://www.kirbyfooty.com/simplemapi.py +Comments: Works (and tested) with: + Outlook Express, Outlook 97 and 2000, + Eudora, Incredimail and Mozilla Thunderbird (1.5.0.2) +Thanks : Werner F. Bruhin and Michele Petrazzo on the ctypes list. + Lukas Lalinsky for patches and encouragement. + +If you have any bug-fixes, enhancements or suggestions regarding this +software, please contact me at the above email address. +""" + +import os +from ctypes import * + +FLAGS = c_ulong +LHANDLE = c_ulong +LPLHANDLE = POINTER(LHANDLE) + +# Return codes +SUCCESS_SUCCESS = 0 +MAPI_USER_ABORT = 1 +MAPI_E_USER_ABORT = MAPI_USER_ABORT +MAPI_E_FAILURE = 2 +MAPI_E_LOGON_FAILURE = 3 +MAPI_E_LOGIN_FAILURE = MAPI_E_LOGON_FAILURE +MAPI_E_DISK_FULL = 4 +MAPI_E_INSUFFICIENT_MEMORY = 5 +MAPI_E_ACCESS_DENIED = 6 +MAPI_E_TOO_MANY_SESSIONS = 8 +MAPI_E_TOO_MANY_FILES = 9 +MAPI_E_TOO_MANY_RECIPIENTS = 10 +MAPI_E_ATTACHMENT_NOT_FOUND = 11 +MAPI_E_ATTACHMENT_OPEN_FAILURE = 12 +MAPI_E_ATTACHMENT_WRITE_FAILURE = 13 +MAPI_E_UNKNOWN_RECIPIENT = 14 +MAPI_E_BAD_RECIPTYPE = 15 +MAPI_E_NO_MESSAGES = 16 +MAPI_E_INVALID_MESSAGE = 17 +MAPI_E_TEXT_TOO_LARGE = 18 +MAPI_E_INVALID_SESSION = 19 +MAPI_E_TYPE_NOT_SUPPORTED = 20 +MAPI_E_AMBIGUOUS_RECIPIENT = 21 +MAPI_E_AMBIG_RECIP = MAPI_E_AMBIGUOUS_RECIPIENT +MAPI_E_MESSAGE_IN_USE = 22 +MAPI_E_NETWORK_FAILURE = 23 +MAPI_E_INVALID_EDITFIELDS = 24 +MAPI_E_INVALID_RECIPS = 25 +MAPI_E_NOT_SUPPORTED = 26 +# Recipient class +MAPI_ORIG = 0 +MAPI_TO = 1 +# Send flags +MAPI_LOGON_UI = 1 +MAPI_DIALOG = 8 + +class MapiRecipDesc(Structure): + _fields_ = [ + ('ulReserved', c_ulong), + ('ulRecipClass', c_ulong), + ('lpszName', c_char_p), + ('lpszAddress', c_char_p), + ('ulEIDSize', c_ulong), + ('lpEntryID', c_void_p), + ] +lpMapiRecipDesc = POINTER(MapiRecipDesc) +lppMapiRecipDesc = POINTER(lpMapiRecipDesc) + +class MapiFileDesc(Structure): + _fields_ = [ + ('ulReserved', c_ulong), + ('flFlags', c_ulong), + ('nPosition', c_ulong), + ('lpszPathName', c_char_p), + ('lpszFileName', c_char_p), + ('lpFileType', c_void_p), + ] +lpMapiFileDesc = POINTER(MapiFileDesc) + +class MapiMessage(Structure): + _fields_ = [ + ('ulReserved', c_ulong), + ('lpszSubject', c_char_p), + ('lpszNoteText', c_char_p), + ('lpszMessageType', c_char_p), + ('lpszDateReceived', c_char_p), + ('lpszConversationID', c_char_p), + ('flFlags', FLAGS), + ('lpOriginator', lpMapiRecipDesc), + ('nRecipCount', c_ulong), + ('lpRecips', lpMapiRecipDesc), + ('nFileCount', c_ulong), + ('lpFiles', lpMapiFileDesc), + ] +lpMapiMessage = POINTER(MapiMessage) + +MAPI = windll.mapi32 +MAPISendMail = MAPI.MAPISendMail +MAPISendMail.restype = c_ulong +MAPISendMail.argtypes = (LHANDLE, c_ulong, lpMapiMessage, FLAGS, c_ulong) + +MAPIResolveName = MAPI.MAPIResolveName +MAPIResolveName.restype = c_ulong +MAPIResolveName.argtypes= (LHANDLE, c_ulong, c_char_p, FLAGS, c_ulong, lppMapiRecipDesc) + +MAPIFreeBuffer = MAPI.MAPIFreeBuffer +MAPIFreeBuffer.restype = c_ulong +MAPIFreeBuffer.argtypes = (c_void_p, ) + +MAPILogon = MAPI.MAPILogon +MAPILogon.restype = c_ulong +MAPILogon.argtypes = (LHANDLE, c_char_p, c_char_p, FLAGS, c_ulong, LPLHANDLE) + +MAPILogoff = MAPI.MAPILogoff +MAPILogoff.restype = c_ulong +MAPILogoff.argtypes = (LHANDLE, c_ulong, FLAGS, c_ulong) + + +class MAPIError(WindowsError): + + def __init__(self, code): + WindowsError.__init__(self) + self.code = code + + def __str__(self): + return 'MAPI error %d' % (self.code,) + + +def _logon(profileName=None, password=None): + pSession = LHANDLE() + rc = MAPILogon(0, profileName, password, MAPI_LOGON_UI, 0, byref(pSession)) + if rc != SUCCESS_SUCCESS: + raise MAPIError, rc + return pSession + + +def _logoff(session): + rc = MAPILogoff(session, 0, 0, 0) + if rc != SUCCESS_SUCCESS: + raise MAPIError, rc + + +def _resolveName(session, name): + pRecipDesc = lpMapiRecipDesc() + rc = MAPIResolveName(session, 0, name, 0, 0, byref(pRecipDesc)) + if rc != SUCCESS_SUCCESS: + raise MAPIError, rc + rd = pRecipDesc.contents + name, address = rd.lpszName, rd.lpszAddress + rc = MAPIFreeBuffer(pRecipDesc) + if rc != SUCCESS_SUCCESS: + raise MAPIError, rc + return name, address + + +def _sendMail(session, recipient, subject, body, attach, preview): + nFileCount = len(attach) + if attach: + MapiFileDesc_A = MapiFileDesc * len(attach) + fda = MapiFileDesc_A() + for fd, fa in zip(fda, attach): + fd.ulReserved = 0 + fd.flFlags = 0 + fd.nPosition = -1 + fd.lpszPathName = fa + fd.lpszFileName = None + fd.lpFileType = None + lpFiles = fda + else: + lpFiles = lpMapiFileDesc() + + RecipWork = recipient.split(';') + RecipCnt = len(RecipWork) + MapiRecipDesc_A = MapiRecipDesc * len(RecipWork) + rda = MapiRecipDesc_A() + for rd, ra in zip(rda, RecipWork): + rd.ulReserved = 0 + rd.ulRecipClass = MAPI_TO + try: + rd.lpszName, rd.lpszAddress = _resolveName(session, ra) + except WindowsError: + # work-round for Mozilla Thunderbird + rd.lpszName, rd.lpszAddress = None, ra + rd.ulEIDSize = 0 + rd.lpEntryID = None + recip = rda + + msg = MapiMessage(0, subject, body, None, None, None, 0, lpMapiRecipDesc(), + RecipCnt, recip, + nFileCount, lpFiles) + flags = 0 + if preview: + flags = MAPI_DIALOG + rc = MAPISendMail(session, 0, byref(msg), flags, 0) + if rc != SUCCESS_SUCCESS: + raise MAPIError, rc + + +def SendMail(recipient, subject="", body="", attachfiles="", preview=1): + """Post an e-mail message using Simple MAPI + + recipient - string: address to send to (multiple addresses separated with a semicolon) + subject - string: subject header + body - string: message text + attach - string: files to attach (multiple attachments separated with a semicolon) + preview - bool : if false, minimise user interaction. Default:true + """ + + attach = [] + AttachWork = attachfiles.split(';') + for filename in AttachWork: + if os.path.exists(filename): + attach.append(filename) + attach = map(os.path.abspath, attach) + + restore = os.getcwd() + try: + session = _logon() + try: + _sendMail(session, recipient, subject, body, attach, preview) + finally: + _logoff(session) + finally: + os.chdir(restore) + + +if __name__ == '__main__': + import sys + recipient = "test@johnnypops.demon.co.uk" + subject = "Test Message Subject" + body = "Hi,\r\n\r\nthis is a quick test message,\r\n\r\ncheers,\r\nJohn." + attachment = sys.argv[0] + SendMail(recipient, subject, body, attachment) + +