1 # -*- coding: utf-8 -*-
3 # (c) Copyright 2010 Hewlett-Packard Development Company, L.P.
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 from __future__ import generators
30 from cStringIO import StringIO
35 from base.codes import *
36 from base.ldif import LDIFParser
37 from base import device, utils, vcard
48 log.error("dbus is required for PC send fax.")
51 # Ignore: .../dbus/connection.py:242: DeprecationWarning: object.__init__() takes no parameters
52 # (occurring on Python 2.6/dBus 0.83/Ubuntu 9.04)
53 warnings.simplefilter("ignore", DeprecationWarning)
56 # Update queue values (Send thread ==> UI)
58 STATUS_PROCESSING_FILES = 1
59 STATUS_SENDING_TO_RECIPIENT = 2
64 STATUS_CREATING_COVER_PAGE = 7
68 STATUS_ERROR_IN_CONNECTING = 11
69 STATUS_ERROR_IN_TRANSMITTING = 12
70 STATUS_ERROR_PROBLEM_IN_FAXLINE = 13
71 STATUS_JOB_CANCEL = 14
73 # Event queue values (UI ==> Send thread)
74 EVENT_FAX_SEND_CANCELED = 1
75 # Other values in queue are:
76 #EVENT_FAX_RENDER_COMPLETE_BEGIN = 8010
77 #EVENT_FAX_RENDER_COMPLETE_SENDDATA = 8011
78 #EVENT_FAX_RENDER_COMPLETE_END = 8012
80 # **************************************************************************** #
81 # HPLIP G3 Fax File Format (big endian)
83 # #==============================================#
84 # # File Header: Total 28 bytes #
85 # #..............................................#
86 # # Magic bytes: 8 bytes ("hplip_g3") #
87 # # Format version: 8 bits (1) #
88 # # Total pages in file(=p): 32 bits #
89 # # Hort DPI: 16 bits (200 or 300) #
90 # # Vert DPI: 16 bits (100, 200, or 300) #
91 # # Page Size: 8 bits (0=Unk, 1=Letter, 2=A4, #
93 # # Resolution: 8 bits (0=Unk, 1=Std, 2=Fine, #
95 # # Encoding: 8 bits (2=MH, 4=MMR, 7=JPEG) #
96 # # Reserved1: 32 bits (0) #
97 # # Reserved2: 32 bits (0) #
98 # #----------------------------------------------#
99 # # Page 1 Header: Total 24 bytes #
100 # #..............................................#
101 # # Page number: 32 bits (1 based) #
102 # # Pixels per row: 32 bits #
103 # # Rows this page: 32 bits #
104 # # Image bytes this page(=x): 32 bits #
105 # # Thumbnail bytes this page(=y): 32 bits #
106 # # (thumbnail not present if y == 0) #
108 # # letter: 134 px wide x 173 px high #
109 # # legal: 134 px wide x 221 px high #
110 # # a4 : 134 px wide x 190 px high #
111 # # Reserved3: 32 bits (0) #
112 # #..............................................#
113 # # Image data: x bytes #
114 # #..............................................#
115 # # Thumbnail data: y bytes (if present) #
116 # #----------------------------------------------#
117 # # Page 2 Header: Total 24 bytes #
118 # #..............................................#
120 # #..............................................#
121 # # Thumbnail data (if present) #
122 # #----------------------------------------------#
123 # # ... Pages 3 - (p-1) ... #
124 # #----------------------------------------------#
125 # # Page p Header: Total 24 bytes #
126 # #..............................................#
128 # #..............................................#
129 # # Thumbnail data (if present) #
130 # #==============================================#
135 RESOLUTION_300DPI = 3
137 FILE_HEADER_SIZE = 28
138 PAGE_HEADER_SIZE = 24
139 # **************************************************************************** #
141 ##skip_dn = ["uid=foo,ou=People,dc=example,dc=com",
142 ## "uid=bar,ou=People,dc=example,dc=com", "dc=example,dc=com"]
144 class FaxLDIFParser(LDIFParser):
145 def __init__(self, input, db):
146 LDIFParser.__init__(self, input)
149 def handle(self, dn, entry):
152 firstname = entry['givenName'][0]
155 firstname = entry['givenname'][0]
160 lastname = entry['sn'][0]
165 nickname = entry['cn'][0]
167 nickname = firstname + ' ' + lastname
170 fax = entry['facsimiletelephonenumber'][0] # fax
173 fax = entry['fax'][0]
184 groups = [g for g in grps if g]
187 log.debug("Import: name=%s, fax=%s, group(s)=%s, notes=%s" % ( nickname, fax, ','.join(groups), dn))
188 self.db.set(nickname, title, firstname, lastname, fax, groups, dn)
192 # **************************************************************************** #
193 class FaxAddressBook(object): # Pickle based address book
197 # { 'name' : {'name': u'',
198 # 'firstname' : u'', # NOT USED STARTING IN 2.8.9
199 # 'lastname': u', # NOT USED STARTING IN 2.8.9
200 # 'title' : u'', # NOT USED STARTING IN 2.8.9
202 # 'groups' : [u'', u'', ...],
203 # 'notes' : u'', } ...
209 self._fab = "/dev/null"
210 if prop.user_dir != None:
211 self._fab = os.path.join(prop.user_dir, "fab.pickle")
212 #old_fab = os.path.join(prop.user_dir, "fab.db")
214 # Load the existing pickle if present
215 if os.path.exists(self._fab):
216 pickle_file = open(self._fab, "r")
217 self._data = cPickle.load(pickle_file)
220 self.save() # save the empty file to create the file
223 def set(self, name, title, firstname, lastname, fax, groups, notes):
225 grps = [unicode(s) for s in groups]
226 except UnicodeDecodeError:
227 grps = [unicode(s.decode('utf-8')) for s in groups]
229 self._data[unicode(name)] = {'name' : unicode(name),
230 'title': unicode(title), # NOT USED STARTING IN 2.8.9
231 'firstname': unicode(firstname), # NOT USED STARTING IN 2.8.9
232 'lastname': unicode(lastname), # NOT USED STARTING IN 2.8.9
234 'notes': unicode(notes),
242 def set_key_value(self, name, key, value):
243 self._data[unicode(name)][key] = value
248 return self._data.get(name, None)
252 def rename(self, old_name, new_name):
261 self._data[new_name] = self._data[old_name].copy()
262 self._data[new_name]['name'] = new_name
263 del self._data[old_name]
267 def get_all_groups(self):
269 for e, v in self._data.items():
270 for g in v['groups']:
271 if g not in all_groups:
276 def get_all_records(self):
280 def get_all_names(self):
281 return self._data.keys()
286 pickle_file = open(self._fab, "w")
287 cPickle.dump(self._data, pickle_file, cPickle.HIGHEST_PROTOCOL)
290 log.error("I/O error saving fab file.")
298 def delete(self, name):
299 if name in self._data:
307 def last_modification_time(self):
309 return os.stat(self._fab).st_mtime
314 def update_groups(self, group, members):
315 for e, v in self._data.items():
316 if v['name'] in members: # membership indicated
317 if not group in v['groups']:
318 v['groups'].append(unicode(group))
320 if group in v['groups']:
321 v['groups'].remove(unicode(group))
325 def delete_group(self, group):
326 for e, v in self._data.items():
327 if group in v['groups']:
328 v['groups'].remove(unicode(group))
332 def group_members(self, group):
334 for e, v in self._data.items():
335 if group in v['groups']:
340 def add_to_group(self, group, members):
341 group_members = self.group_members(group)
342 new_group_members = []
344 if m not in group_members:
345 new_group_members.append(m)
347 self.update_groups(group, group_members + new_group_members)
350 def remove_from_group(self, group, remove_members):
351 group_members = self.group_members(group)
352 new_group_members = []
353 for m in group_members:
354 if m not in remove_members:
355 new_group_members.append(m)
357 self.update_groups(group, new_group_members)
360 def rename_group(self, old_group, new_group):
361 members = self.group_members(old_group)
362 self.update_groups(old_group, [])
363 self.update_groups(new_group, members)
366 def import_ldif(self, filename):
368 data = open(filename, 'r').read()
369 log.debug_block(filename, data)
370 parser = FaxLDIFParser(open(filename, 'r'), self)
374 except ValueError, e:
375 return False, e.message
378 def import_vcard(self, filename):
379 data = file(filename, 'r').read()
380 log.debug_block(filename, data)
382 for card in vcard.VCards(vcard.VFile(vcard.opentextfile(filename))):
387 for x in range(1, 9999):
398 if 'fax' in card[s]['type']:
399 fax = card[s]['number']
402 org = card.get('organisation', '')
406 org = card.get('categories', '').split(';')
411 groups = [o for o in org if o]
414 notes = card.get('notes', u'')
415 log.debug("Import: name=%s, fax=%s group(s)=%s notes=%s" % (name, fax, ','.join(groups), notes))
416 self.set(name, u'', u'', u'', fax, groups, notes)
421 # **************************************************************************** #
422 class FaxDevice(device.Device):
424 def __init__(self, device_uri=None, printer_name=None,
426 fax_type=FAX_TYPE_NONE,
429 device.Device.__init__(self, device_uri, printer_name,
430 None, callback, disable_dbus)
432 self.send_fax_thread = None
433 self.upload_log_thread = None
434 self.fax_type = fax_type
437 session_bus = dbus.SessionBus()
438 self.service = session_bus.get_object('com.hplip.StatusService', "/com/hplip/StatusService")
443 def setPhoneNum(self, num):
446 def getPhoneNum(self):
449 phone_num = property(getPhoneNum, setPhoneNum)
452 def setStationName(self, name):
455 def getStationName(self):
458 station_name = property(getStationName, setStationName)
460 def setDateAndTime(self):
466 def isUploadLogActive(self):
469 def waitForUploadLogThread(self):
472 def sendFaxes(self, phone_num_list, fax_file_list, cover_message='', cover_re='',
473 cover_func=None, preserve_formatting=False, printer_name='',
474 update_queue=None, event_queue=None):
478 def isSendFaxActive(self):
479 if self.send_fax_thread is not None:
480 return self.send_fax_thread.isAlive()
484 def waitForSendFaxThread(self):
485 if self.send_fax_thread is not None and \
486 self.send_fax_thread.isAlive():
489 self.send_fax_thread.join()
490 except KeyboardInterrupt:
494 # **************************************************************************** #
497 def getFaxDevice(device_uri=None, printer_name=None,
499 fax_type=FAX_TYPE_NONE,
502 if fax_type == FAX_TYPE_NONE:
503 if device_uri is None and printer_name is not None:
504 printers = cups.getPrinters()
507 if p.name.lower() == printer_name.lower():
508 device_uri = p.device_uri
511 raise Error(ERROR_DEVICE_NOT_FOUND)
513 if device_uri is not None:
514 mq = device.queryModelByURI(device_uri)
515 fax_type = mq['fax-type']
517 log.debug("fax-type=%d" % fax_type)
519 if fax_type in (FAX_TYPE_BLACK_SEND_EARLY_OPEN, FAX_TYPE_BLACK_SEND_LATE_OPEN):
520 from pmlfax import PMLFaxDevice
521 return PMLFaxDevice(device_uri, printer_name, callback, fax_type, disable_dbus)
523 elif fax_type == FAX_TYPE_SOAP:
524 from soapfax import SOAPFaxDevice
525 return SOAPFaxDevice(device_uri, printer_name, callback, fax_type, disable_dbus)
527 elif fax_type == FAX_TYPE_LEDMSOAP:
528 from ledmsoapfax import LEDMSOAPFaxDevice
529 return LEDMSOAPFaxDevice(device_uri, printer_name, callback, fax_type, disable_dbus)
531 elif fax_type == FAX_TYPE_MARVELL:
532 from marvellfax import MarvellFaxDevice
533 return MarvellFaxDevice(device_uri, printer_name, callback, fax_type, disable_dbus)
535 elif fax_type == FAX_TYPE_LEDM:
536 from ledmfax import LEDMFaxDevice
537 return LEDMFaxDevice(device_uri, printer_name, callback, fax_type, disable_dbus)
540 raise Error(ERROR_DEVICE_DOES_NOT_SUPPORT_OPERATION)
542 # **************************************************************************** #
547 # TODO: Define these in only 1 place!
552 STATE_READ_SENDER_INFO = 30
554 STATE_COUNT_PAGES = 50
555 STATE_NEXT_RECIPIENT = 60
556 STATE_COVER_PAGE = 70
557 STATE_SINGLE_FILE = 80
558 STATE_MERGE_FILES = 90
559 STATE_SINGLE_FILE = 100
564 class FaxSendThread(threading.Thread):
565 def __init__(self, dev, service, phone_num_list, fax_file_list,
566 cover_message='', cover_re='', cover_func=None, preserve_formatting=False,
567 printer_name='', update_queue=None, event_queue=None):
569 threading.Thread.__init__(self)
571 self.dev = dev # device.Device
572 self.service = service # dbus proxy to status server object
573 self.phone_num_list = phone_num_list
574 self.fax_file_list = fax_file_list
575 self.update_queue = update_queue
576 self.event_queue = event_queue
577 self.cover_message = cover_message
578 self.cover_re = cover_re
579 self.cover_func = cover_func
580 self.current_printer = printer_name
581 self.stream = StringIO()
582 self.prev_update = ''
583 self.remove_temp_file = False
584 self.preserve_formatting = preserve_formatting
585 self.results = {} # {'file' : error_code,...}
586 self.cover_page_present = False
587 self.recipient_file_list = []
588 self.f = None # final file of fax data to send (pages merged)
589 self.job_hort_dpi = 0
590 self.job_hort_dpi = 0
591 self.job_vert_dpi = 0
592 self.job_page_size = 0
593 self.job_resolution = 0
594 self.job_encoding = 0
597 def pre_render(self, state):
598 # pre-render each page that needs rendering
599 # except for the cover page
600 self.cover_page_present = False
601 log.debug(self.fax_file_list)
603 for fax_file in self.fax_file_list: # (file, type, desc, title)
604 fax_file_name, fax_file_type, fax_file_desc, \
605 fax_file_title, fax_file_pages = fax_file
607 if fax_file_type == "application/hplip-fax-coverpage": # render later
608 self.cover_page_present = True
609 log.debug("Skipping coverpage")
611 #if fax_file_type == "application/hplip-fax": # already rendered
613 self.rendered_file_list.append((fax_file_name, "application/hplip-fax",
614 "HP Fax", fax_file_title))
616 log.debug("Processing pre-rendered file: %s (%d pages)" %
617 (fax_file_name, fax_file_pages))
619 if self.check_for_cancel():
620 state = STATE_ABORTED
622 log.debug(self.rendered_file_list)
624 if self.check_for_cancel():
625 state = STATE_ABORTED
630 def count_pages(self, state):
631 self.recipient_file_list = self.rendered_file_list[:]
632 log.debug("Counting total pages...")
633 self.job_total_pages = 0
634 log.debug(self.recipient_file_list)
637 for fax_file in self.recipient_file_list: # (file, type, desc, title)
638 fax_file_name = fax_file[0]
639 log.debug("Processing file (counting pages): %s..." % fax_file_name)
641 #self.write_queue((STATUS_PROCESSING_FILES, self.job_total_pages, ''))
643 if os.path.exists(fax_file_name):
644 self.results[fax_file_name] = ERROR_SUCCESS
645 fax_file_fd = file(fax_file_name, 'r')
646 header = fax_file_fd.read(FILE_HEADER_SIZE)
648 magic, version, total_pages, hort_dpi, vert_dpi, page_size, \
649 resolution, encoding, reserved1, reserved2 = \
650 self.decode_fax_header(header)
652 if magic != 'hplip_g3':
653 log.error("Invalid file header. Bad magic.")
654 self.results[fax_file_name] = ERROR_FAX_INVALID_FAX_FILE
659 self.job_hort_dpi, self.job_vert_dpi, self.job_page_size, \
660 self.job_resolution, self.job_encoding = \
661 hort_dpi, vert_dpi, page_size, resolution, encoding
665 if self.job_hort_dpi != hort_dpi or \
666 self.job_vert_dpi != vert_dpi or \
667 self.job_page_size != page_size or \
668 self.job_resolution != resolution or \
669 self.job_encoding != encoding:
671 log.error("Incompatible options for file: %s" % fax_file_name)
672 self.results[fax_file_name] = ERROR_FAX_INCOMPATIBLE_OPTIONS
676 log.debug("Magic=%s Ver=%d Pages=%d hDPI=%d vDPI=%d Size=%d Res=%d Enc=%d" %
677 (magic, version, total_pages, hort_dpi,
678 vert_dpi, page_size, resolution, encoding))
680 self.job_total_pages += total_pages
685 log.error("Unable to find HP Fax file: %s" % fax_file_name)
686 self.results[fax_file_name] = ERROR_FAX_FILE_NOT_FOUND
690 if self.check_for_cancel():
691 state = STATE_ABORTED
695 if self.cover_page_present:
696 self.job_total_pages += 1 # Cover pages are truncated to 1 page
698 log.debug("Total fax pages=%d" % self.job_total_pages)
702 def decode_fax_header(self, header):
704 return struct.unpack(">8sBIHHBBBII", header)
706 return -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
708 def decode_page_header(self, header):
710 return struct.unpack(">IIIIII", header)
712 return -1, -1, -1, -1, -1, -1
714 def cover_page(self, recipient):
715 if self.job_total_pages > 1:
716 state = STATE_MERGE_FILES
718 state = STATE_SINGLE_FILE
720 if self.cover_page_present:
721 log.debug("Creating cover page for recipient: %s" % recipient['name'])
722 fax_file, canceled = self.render_cover_page(recipient)
725 state = STATE_ABORTED
727 state = STATE_ERROR # timeout
729 self.recipient_file_list.insert(0, (fax_file, "application/hplip-fax",
730 "HP Fax", 'Cover Page'))
732 log.debug("Cover page G3 file: %s" % fax_file)
734 self.results[fax_file] = ERROR_SUCCESS
738 def single_file(self, state):
739 state = STATE_SEND_FAX
741 log.debug("Processing single file...")
742 self.f = self.recipient_file_list[0][0]
745 f_fd = file(self.f, 'r')
747 log.error("Unable to open fax file: %s" % self.f)
750 header = f_fd.read(FILE_HEADER_SIZE)
752 magic, version, total_pages, hort_dpi, vert_dpi, page_size, \
753 resolution, encoding, reserved1, reserved2 = self.decode_fax_header(header)
755 self.results[self.f] = ERROR_SUCCESS
757 if magic != 'hplip_g3':
758 log.error("Invalid file header. Bad magic.")
759 self.results[self.f] = ERROR_FAX_INVALID_FAX_FILE
762 log.debug("Magic=%s Ver=%d Pages=%d hDPI=%d vDPI=%d Size=%d Res=%d Enc=%d" %
763 (magic, version, total_pages, hort_dpi, vert_dpi,
764 page_size, resolution, encoding))
771 def merge_files(self, state):
772 log.debug("%s State: Merge multiple files" % ("*"*20))
773 log.debug(self.recipient_file_list)
774 log.debug("Merging g3 files...")
775 self.remove_temp_file = True
777 if self.job_total_pages:
778 f_fd, self.f = utils.make_temp_file()
779 log.debug("Temp file=%s" % self.f)
781 data = struct.pack(">8sBIHHBBBII", "hplip_g3", 1L, self.job_total_pages,
782 self.job_hort_dpi, self.job_vert_dpi, self.job_page_size,
783 self.job_resolution, self.job_encoding,
790 for fax_file in self.recipient_file_list:
791 fax_file_name = fax_file[0]
792 log.debug("Processing file: %s..." % fax_file_name)
794 if self.results[fax_file_name] == ERROR_SUCCESS:
795 fax_file_fd = file(fax_file_name, 'r')
796 header = fax_file_fd.read(FILE_HEADER_SIZE)
798 magic, version, total_pages, hort_dpi, vert_dpi, page_size, \
799 resolution, encoding, reserved1, reserved2 = self.decode_fax_header(header)
801 if magic != 'hplip_g3':
802 log.error("Invalid file header. Bad magic.")
806 log.debug("Magic=%s Ver=%d Pages=%d hDPI=%d vDPI=%d Size=%d Res=%d Enc=%d" %
807 (magic, version, total_pages, hort_dpi, vert_dpi, page_size, resolution, encoding))
809 for p in range(total_pages):
810 header = fax_file_fd.read(PAGE_HEADER_SIZE)
812 page_num, ppr, rpp, bytes_to_read, thumbnail_bytes, reserved2 = \
813 self.decode_page_header(header)
816 log.error("Page header error")
820 header = struct.pack(">IIIIII", job_page_num, ppr, rpp, bytes_to_read, thumbnail_bytes, 0L)
821 os.write(f_fd, header)
823 self.write_queue((STATUS_PROCESSING_FILES, job_page_num, ''))
825 log.debug("Page=%d PPR=%d RPP=%d BPP=%d Thumb=%s" %
826 (page_num, ppr, rpp, bytes_to_read, thumbnail_bytes))
828 os.write(f_fd, fax_file_fd.read(bytes_to_read))
833 if self.check_for_cancel():
834 state = STATE_ABORTED
838 log.error("Skipping file: %s" % fax_file_name)
842 log.debug("Total pages=%d" % self.job_total_pages)
847 def next_recipient_gen(self):
848 for a in self.phone_num_list:
851 def next_file_gen(self):
852 for a in self.recipient_file_list:
856 def render_file(self, path, title, mime_type, force_single_page=False):
864 if mime_type in ["application/x-cshell",
865 "application/x-perl",
866 "application/x-python",
867 "application/x-shell",
871 cups.addOption('prettyprint')
874 cups.addOption('number-up=%d' % nup)
876 if force_single_page:
877 cups.addOption('page-ranges=1') # Force coverpage to 1 page
879 sent_job_id = cups.printFile(self.current_printer, path, title)
882 log.debug("Job ID=%d" % sent_job_id)
890 end_time = time.time() + 300.0 # wait for 5 min. max
891 while time.time() < end_time:
892 log.debug("Waiting for fax...")
894 result = list(self.service.CheckForWaitingFax(self.dev.device_uri, prop.username, sent_job_id))
896 fax_file = str(result[7])
897 log.debug("Fax file=%s" % fax_file)
902 if self.check_for_cancel():
903 log.error("Render canceled. Canceling job #%d..." % sent_job_id)
904 cups.cancelJob(sent_job_id)
910 log.error("Timeout waiting for rendering. Canceling job #%d..." % sent_job_id)
911 cups.cancelJob(sent_job_id)
914 return fax_file, False
917 def check_for_cancel(self):
919 while self.event_queue.qsize():
921 event = self.event_queue.get(0)
922 if event[0] == EVENT_FAX_SEND_CANCELED:
924 log.debug("Cancel pressed!")
930 def render_cover_page(self, a):
931 log.debug("Creating cover page...")
933 pdf = self.cover_func(page_size=coverpages.PAGE_SIZE_LETTER,
934 total_pages=self.job_total_pages,
936 recipient_name=a['name'],
937 recipient_phone='', # ???
938 recipient_fax=a['fax'],
940 sender_name=self.sender_name,
941 sender_phone=user_conf.get('fax', 'voice_phone'),
942 sender_fax=self.sender_fax,
943 sender_email=user_conf.get('fax', 'email_address'),
945 regarding=self.cover_re,
946 message=self.cover_message,
947 preserve_formatting=self.preserve_formatting)
949 log.debug("PDF File=%s" % pdf)
950 fax_file, canceled = self.render_file(pdf, 'Cover Page', "application/pdf",
951 force_single_page=True)
958 return fax_file, canceled
961 def write_queue(self, message):
962 if self.update_queue is not None and message != self.prev_update:
963 self.update_queue.put(message)
965 self.prev_update = message