pax_global_header00006660000000000000000000000064127223253320014513gustar00rootroot0000000000000052 comment=25aefb1c9fc05b54ecef374cc95c934344c9bf0e mailnag-1.2.1/000077500000000000000000000000001272232533200131245ustar00rootroot00000000000000mailnag-1.2.1/.gitignore000066400000000000000000000001111272232533200151050ustar00rootroot00000000000000*.pyc locale Mailnag/plugins/goaplugin.py Mailnag/plugins/unityplugin.py mailnag-1.2.1/AUTHORS000066400000000000000000000027011272232533200141740ustar00rootroot00000000000000Maintainer: =========== Patrick Ulbrich Related software: ================= Even though many parts of Mailnag have completely been rewritten by now, Mailnag started out as a fork of Popper (http://launchpad.net/popper). Popper was written by Ralf Hersel . Code, docs and packaging contributors: ====================================== Amin Bandali Edwin Smulders Hasan Yavuz Özderya Leighton Earl Matthias Mailänder Taylor Braun-Jones Thorsten Leemhuis Thomas Haider Vincent Cheng Artwork & icon design: ====================== Reda Lazri Translators (launchpad): ======================== Adolfo Jayme Barrientos Alin Andrei AmiG Anders Jonsson Akihiro Tsukada Asier Sarasua Garmendia Aydın Yakar Bae Taegil dagavi Dmitry Shachnev Einar Uvsløkk elleryq Eugene Marshal Fabrizio Papa Hromin Hu Meng Isamu715 Ivo Majić javiggvv Jean-Marc Jirka Dutka KEIII kristian LEROY Jean-Christophe Lê Trường An Lidinei Lukasz Man from Mars Manuel Xosé Lemos Marcos Lans Marti Bosch Mattia Meneguzzo Мирослав Николић Oleg «Eleidan» Kulyk Patrick Ulbrich Pedro Beja Philippe Poumaroux Piotr Filipek poulp Radek Otáhal Rafael Neri RapierTG Rax Szymon Nieznański Tobias Bannert u-t vbert Vyacheslav Sharmanov Wolter Hellmund Zeppelinlg zmni 朱涛 mailnag-1.2.1/LICENSE000066400000000000000000000432541272232533200141410ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. mailnag-1.2.1/Mailnag/000077500000000000000000000000001272232533200144745ustar00rootroot00000000000000mailnag-1.2.1/Mailnag/__init__.py000066400000000000000000000014711272232533200166100ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # __init__.py # # Copyright 2012 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # mailnag-1.2.1/Mailnag/common/000077500000000000000000000000001272232533200157645ustar00rootroot00000000000000mailnag-1.2.1/Mailnag/common/__init__.py000066400000000000000000000014711272232533200201000ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # __init__.py # # Copyright 2011 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # mailnag-1.2.1/Mailnag/common/accounts.py000066400000000000000000000227471272232533200201710ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # accounts.py # # Copyright 2011 - 2016 Patrick Ulbrich # Copyright 2016 Thomas Haider # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import poplib import logging import Mailnag.common.imaplib2 as imaplib from Mailnag.common.utils import splitstr account_defaults = { 'enabled' : '0', 'name' : '', 'user' : '', 'password' : '', 'server' : '', 'port' : '', 'ssl' : '1', 'imap' : '1', 'idle' : '1', 'folder' : '' } CREDENTIAL_KEY = 'Mailnag password for %s://%s@%s' # # Account class # class Account: def __init__(self, enabled = False, name = '', user = '', \ password = '', oauth2string = '', server = '', port = '', ssl = True, imap = True, idle = True, folders = []): self.enabled = enabled # bool self.name = name self.user = user self.password = password self.oauth2string = oauth2string self.server = server self.port = port self.ssl = ssl # bool self.imap = imap # bool self.idle = idle # bool self.folders = folders self._conn = None def get_connection(self, use_existing = False): # get email server connection if self.imap: return self._get_IMAP_connection(use_existing) else: return self._get_POP3_connection(use_existing) # Indicates whether the account # holds an active existing connection. # Note: this method only indicates if the # account *holds* (caches) an existing connection. # There may be further, but no longer # associated connections if get_connection() # was called multiple times (with use_existing # set to False). def has_connection(self): if self.imap: return self._has_IMAP_connection() else: return self._has_POP3_connection() # Requests folder names (list) from a server. # Relevant for IMAP accounts only. # Returns an empty list when used on POP3 accounts. def request_server_folders(self): lst = [] if not self.imap: return lst # Always create a new connection as an existing one may # be used for IMAP IDLE. conn = self._get_IMAP_connection(use_existing = False) try: status, data = conn.list('', '*') finally: # conn.close() # allowed in SELECTED state only conn.logout() separators = [ ' "/" ', ' "." ' ] for d in data: folder = '' for s in separators: if s in d: folder = d.split(s)[-1] break if len(folder) == 0: logging.warning("Folder format not supported.") break if (folder[0] == '"') and (folder[-1] == '"'): folder = folder[1:-1] lst.append(folder) return lst def get_id(self): # TODO : this id is not really unique... return str(hash(self.user + self.server + ', '.join(self.folders))) def _has_IMAP_connection(self): return (self._conn != None) and \ (self._conn.state != imaplib.LOGOUT) and \ (not self._conn.Terminate) def _get_IMAP_connection(self, use_existing): # try to reuse existing connection if use_existing and self._has_IMAP_connection(): return self._conn self._conn = conn = None try: if self.ssl: if self.port == '': conn = imaplib.IMAP4_SSL(self.server) else: conn = imaplib.IMAP4_SSL(self.server, int(self.port)) else: if self.port == '': conn = imaplib.IMAP4(self.server) else: conn = imaplib.IMAP4(self.server, int(self.port)) if 'STARTTLS' in conn.capabilities: conn.starttls() else: logging.warning("Using unencrypted connection for account '%s'" % self.name) if self.oauth2string != '': conn.authenticate('XOAUTH2', lambda x: self.oauth2string) else: conn.login(self.user, self.password) self._conn = conn except: try: if conn != None: # conn.close() # allowed in SELECTED state only conn.logout() except: pass raise # re-throw exception return self._conn def _has_POP3_connection(self): return (self._conn != None) and \ ('sock' in self._conn.__dict__) def _get_POP3_connection(self, use_existing): # try to reuse existing connection if use_existing and self._has_POP3_connection(): return self._conn self._conn = conn = None try: if self.ssl: if self.port == '': conn = poplib.POP3_SSL(self.server) else: conn = poplib.POP3_SSL(self.server, int(self.port)) else: if self.port == '': conn = poplib.POP3(self.server) else: conn = poplib.POP3(self.server, int(self.port)) # TODO : Use STARTTLS when Mailnag has been migrated to python 3 # (analogous to get_IMAP_connection). logging.warning("Using unencrypted connection for account '%s'" % self.name) conn.getwelcome() conn.user(self.user) conn.pass_(self.password) self._conn = conn except: try: if conn != None: conn.quit() except: pass raise # re-throw exception return self._conn # # AccountManager class # class AccountManager: def __init__(self, credentialstore = None): self._accounts = [] self._removed = [] self._credentialstore = credentialstore def __len__(self): return len(self._accounts) def __iter__(self): for acc in self._accounts: yield acc def __contains__(self, item): return (item in self._accounts) def add(self, account): self._accounts.append(account) def remove(self, account): self._accounts.remove(account) self._removed.append(account) def clear(self): for acc in self._accounts: self._removed.append(acc) del self._accounts[:] def to_list(self): # Don't pass a ref to the internal accounts list. # (Accounts must be removed via the remove() method only.) return self._accounts[:] def load_from_cfg(self, cfg, enabled_only = False): del self._accounts[:] del self._removed[:] i = 1 section_name = "account" + str(i) while cfg.has_section(section_name): enabled = bool(int( self._get_account_cfg(cfg, section_name, 'enabled') )) if (not enabled_only) or (enabled_only and enabled): name = self._get_account_cfg(cfg, section_name, 'name') user = self._get_account_cfg(cfg, section_name, 'user') password = self._get_account_cfg(cfg, section_name, 'password') server = self._get_account_cfg(cfg, section_name, 'server') port = self._get_account_cfg(cfg, section_name, 'port') ssl = bool(int( self._get_account_cfg(cfg, section_name, 'ssl') )) imap = bool(int( self._get_account_cfg(cfg, section_name, 'imap') )) idle = bool(int( self._get_account_cfg(cfg, section_name, 'idle') )) folders = splitstr(self._get_account_cfg(cfg, section_name, 'folder'), ',') if self._credentialstore != None: protocol = 'imap' if imap else 'pop' password = self._credentialstore.get(CREDENTIAL_KEY % (protocol, user, server)) acc = Account(enabled, name, user, password, '', server, port, ssl, imap, idle, folders) self._accounts.append(acc) i = i + 1 section_name = "account" + str(i) def save_to_cfg(self, cfg): # Remove all accounts from cfg i = 1 section_name = "account" + str(i) while cfg.has_section(section_name): cfg.remove_section(section_name) i = i + 1 section_name = "account" + str(i) # Delete secrets of removed accounts from the credential store # (it's important to do this before adding accounts, # in case multiple accounts with the same credential key exist). if self._credentialstore != None: for acc in self._removed: protocol = 'imap' if acc.imap else 'pop' # Note: CredentialStore implementations must check if the key acutally exists! self._credentialstore.remove(CREDENTIAL_KEY % (protocol, acc.user, acc.server)) del self._removed[:] # Add accounts i = 1 for acc in self._accounts: if acc.oauth2string != '': logging.warning("Saving of OAuth2 based accounts is not supported. Account '%s' skipped." % acc.name) continue section_name = "account" + str(i) cfg.add_section(section_name) cfg.set(section_name, 'enabled', int(acc.enabled)) cfg.set(section_name, 'name', acc.name) cfg.set(section_name, 'user', acc.user) cfg.set(section_name, 'password', '') cfg.set(section_name, 'server', acc.server) cfg.set(section_name, 'port', acc.port) cfg.set(section_name, 'ssl', int(acc.ssl)) cfg.set(section_name, 'imap', int(acc.imap)) cfg.set(section_name, 'idle', int(acc.idle)) cfg.set(section_name, 'folder', ', '.join(acc.folders)) if self._credentialstore != None: protocol = 'imap' if acc.imap else 'pop' self._credentialstore.set(CREDENTIAL_KEY % (protocol, acc.user, acc.server), acc.password) else: cfg.set(section_name, 'password', acc.password) i = i + 1 def _get_account_cfg(self, cfg, section_name, option_name): if cfg.has_option(section_name, option_name): return cfg.get(section_name, option_name) else: return account_defaults[option_name] mailnag-1.2.1/Mailnag/common/config.py000066400000000000000000000032051272232533200176030ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # config.py # # Copyright 2011 - 2015 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import os import xdg.BaseDirectory as bd from ConfigParser import RawConfigParser mailnag_defaults = { 'core': { 'poll_interval' : '10', 'imap_idle_timeout' : '10', 'autostart' : '1', 'connectivity_test' : 'auto', 'credentialstore' : 'auto', 'enabled_plugins' : 'dbusplugin, soundplugin, libnotifyplugin' } } cfg_folder = os.path.join(bd.xdg_config_home, "mailnag") cfg_file = os.path.join(cfg_folder, "mailnag.cfg") def cfg_exists(): return os.path.exists(cfg_file) def read_cfg(): cfg = RawConfigParser() cfg._sections = mailnag_defaults # HACK : use cfg.read_dict(mailnag_defaults) in python 3 if os.path.exists(cfg_file): cfg.read(cfg_file) return cfg def write_cfg(cfg): if not os.path.exists(cfg_folder): os.makedirs(cfg_folder) with open(cfg_file, 'wb') as configfile: cfg.write(configfile) mailnag-1.2.1/Mailnag/common/credentialstore.py000066400000000000000000000107121272232533200215260ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # credentialstore.py # # Copyright 2015, 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import hashlib # TODO: Make this class an enum # when Mailnag is ported to python 3 class CredentialStoreType: NONE = 'none' GNOME = 'gnome' # KDE = 'kde' _credentialstoretype = CredentialStoreType.NONE try: import gi gi.require_version('GnomeKeyring', '1.0') from gi.repository import GnomeKeyring _credentialstoretype = CredentialStoreType.GNOME except: pass # # CredentialStore base class # class CredentialStore: _instance = None def set(self, key, secret): pass def get(self, key): pass def remove(self, key): pass @staticmethod def get_default(): if (CredentialStore._instance == None) and (_credentialstoretype != CredentialStoreType.NONE): CredentialStore._instance = SupportedCredentialStores[_credentialstoretype]() return CredentialStore._instance @staticmethod def from_string(strn): cs = None if strn == 'auto': cs = CredentialStore.get_default() elif strn in SupportedCredentialStores: cs = SupportedCredentialStores[strn]() return cs # # GNOME CredentialStore # class GnomeCredentialStore(CredentialStore): def __init__(self): (result, kr_name) = GnomeKeyring.get_default_keyring_sync() self._defaultKeyring = kr_name if self._defaultKeyring == None: self._defaultKeyring = 'login' result = GnomeKeyring.unlock_sync(self._defaultKeyring, None) if result != GnomeKeyring.Result.OK: raise KeyringUnlockException('Failed to unlock default keyring') self._migrate_keyring() def get(self, key): attrs = self._get_attrs(key) result, items = GnomeKeyring.find_items_sync(GnomeKeyring.ItemType.GENERIC_SECRET, attrs) if result == GnomeKeyring.Result.OK: return items[0].secret else: return '' def set(self, key, secret): if secret == '': return attrs = self._get_attrs(key) existing_secret = '' result, items = GnomeKeyring.find_items_sync(GnomeKeyring.ItemType.GENERIC_SECRET, attrs) if result == GnomeKeyring.Result.OK: existing_secret = items[0].secret if existing_secret != secret: GnomeKeyring.item_create_sync(self._defaultKeyring, \ GnomeKeyring.ItemType.GENERIC_SECRET, key, \ attrs, secret, True) def remove(self, key): attrs = self._get_attrs(key) result, items = GnomeKeyring.find_items_sync(GnomeKeyring.ItemType.GENERIC_SECRET, attrs) if result == GnomeKeyring.Result.OK: GnomeKeyring.item_delete_sync(self._defaultKeyring, items[0].item_id) def _get_attrs(self, key): attrs = GnomeKeyring.Attribute.list_new() keyid = hashlib.md5(key.encode('utf-8')).hexdigest() GnomeKeyring.Attribute.list_append_string(attrs, 'source', 'Mailnag') GnomeKeyring.Attribute.list_append_string(attrs, 'api-version', '1.1') GnomeKeyring.Attribute.list_append_string(attrs, 'keyid', keyid) return attrs # Migrates pre Mailnag 1.1 keyring items into the new format def _migrate_keyring(self): attrs = GnomeKeyring.Attribute.list_new() GnomeKeyring.Attribute.list_append_string(attrs, 'application', 'Mailnag') result, items = GnomeKeyring.find_items_sync(GnomeKeyring.ItemType.GENERIC_SECRET, attrs) if result == GnomeKeyring.Result.OK: for i in items: result, info = GnomeKeyring.item_get_info_sync(self._defaultKeyring, i.item_id) self.set(info.get_display_name(), i.secret) GnomeKeyring.item_delete_sync(self._defaultKeyring, i.item_id) # # Exception thrown if the GNOME keyring can't be unlocked # class KeyringUnlockException(Exception): def __init__(self, message): Exception.__init__(self, message) # # All supported credential stores # SupportedCredentialStores = { CredentialStoreType.GNOME : GnomeCredentialStore #CredentialStoreType.KDE : KDECredentialStore } mailnag-1.2.1/Mailnag/common/dist_cfg.py000066400000000000000000000042101272232533200201150ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # dist_cfg.py # # Copyright 2012 - 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # # # This file contains constants that need to be adjusted for propper distro integration. # # Application version displayed in the # about dialog of the config window. APP_VERSION = '1.2.1' # The PACKAGE_NAME constant is used to configure # 1) the path where all app data (glade files, images) is loaded from # (usually /usr/share/) via get_data_file() (see utils.py). # 2) paths for localization files generated with gen_locales # (usually /usr/share/locale//LC_MESSAGES/.mo). # Typically, there's no need to touch this constant. PACKAGE_NAME = 'mailnag' # The LOCALE_DIR constant specifies the root path for localization files # (usually you have to make it point to '/usr/share/locale'). LOCALE_DIR = './locale' # The DESKTOP_FILE_DIR constant specifies the root path for .desktop files # (usually you have to make it point to '/usr/share/applications'). DESKTOP_FILE_DIR = './data' # The LIB_DIR constant specifies the root path for the Mailnag python files # (usually you have to make it point to /Mailnag). LIB_DIR = './Mailnag' # The BIN_DIR constant specifies the path for the mailnag start scripts # (usually you have to make it point to '/usr/bin'). BIN_DIR = '.' # DBUS service configuration DBUS_BUS_NAME = 'mailnag.MailnagService' DBUS_OBJ_PATH = '/mailnag/MailnagService' mailnag-1.2.1/Mailnag/common/exceptions.py000066400000000000000000000016521272232533200205230ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # exceptions.py # # Copyright 2014 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # class InvalidOperationException(Exception): def __init__(self, message): Exception.__init__(self, message) mailnag-1.2.1/Mailnag/common/i18n.py000066400000000000000000000022041272232533200171130ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # i18n.py # # Copyright 2011, 2012, 2014 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import locale import gettext from Mailnag.common.dist_cfg import PACKAGE_NAME, LOCALE_DIR # bind textdomain for GTK Builder locale.bindtextdomain(PACKAGE_NAME, LOCALE_DIR) # add gettext shortcut "_" for string translations _ = gettext.translation(domain = PACKAGE_NAME, localedir = LOCALE_DIR, fallback = True).ugettext mailnag-1.2.1/Mailnag/common/imaplib2.py000066400000000000000000002456771272232533200200620ustar00rootroot00000000000000#!/usr/bin/env python2 """Threaded IMAP4 client. Based on RFC 3501 and original imaplib module. Public classes: IMAP4 IMAP4_SSL IMAP4_stream Public functions: Internaldate2Time ParseFlags Time2Internaldate """ __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream", "Internaldate2Time", "ParseFlags", "Time2Internaldate") __version__ = "2.27" __release__ = "2" __revision__ = "27" __credits__ = """ Authentication code contributed by Donn Cave June 1998. String method conversion by ESR, February 2001. GET/SETACL contributed by Anthony Baxter April 2001. IMAP4_SSL contributed by Tino Lange March 2002. GET/SETQUOTA contributed by Andreas Zeidler June 2002. PROXYAUTH contributed by Rick Holbert November 2002. IDLE via threads suggested by Philippe Normand January 2005. GET/SETANNOTATION contributed by Tomas Lindroos June 2005. COMPRESS/DEFLATE contributed by Bron Gondwana May 2009. STARTTLS from Jython's imaplib by Alan Kennedy. ID contributed by Dave Baggett November 2009. Improved untagged responses handling suggested by Dave Baggett November 2009. Improved thread naming, and 0 read detection contributed by Grant Edwards June 2010. Improved timeout handling contributed by Ivan Vovnenko October 2010. Timeout handling further improved by Ethan Glasser-Camp December 2010. Time2Internaldate() patch to match RFC2060 specification of English month names from bugs.python.org/issue11024 March 2011. starttls() bug fixed with the help of Sebastian Spaeth April 2011. Threads now set the "daemon" flag (suggested by offlineimap-project) April 2011. Single quoting introduced with the help of Vladimir Marek August 2011.""" __author__ = "Piers Lauder " __URL__ = "http://imaplib2.sourceforge.net" __license__ = "Python License" import binascii, errno, os, Queue, random, re, select, socket, sys, time, threading, zlib select_module = select # Globals CRLF = '\r\n' Debug = None # Backward compatibility IMAP4_PORT = 143 IMAP4_SSL_PORT = 993 IDLE_TIMEOUT_RESPONSE = '* IDLE TIMEOUT\r\n' IDLE_TIMEOUT = 60*29 # Don't stay in IDLE state longer READ_POLL_TIMEOUT = 30 # Without this timeout interrupted network connections can hang reader READ_SIZE = 32768 # Consume all available in socket DFLT_DEBUG_BUF_LVL = 3 # Level above which the logging output goes directly to stderr AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first # Commands CMD_VAL_STATES = 0 CMD_VAL_ASYNC = 1 NONAUTH, AUTH, SELECTED, LOGOUT = 'NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT' Commands = { # name valid states asynchronous 'APPEND': ((AUTH, SELECTED), False), 'AUTHENTICATE': ((NONAUTH,), False), 'CAPABILITY': ((NONAUTH, AUTH, SELECTED), True), 'CHECK': ((SELECTED,), True), 'CLOSE': ((SELECTED,), False), 'COMPRESS': ((AUTH,), False), 'COPY': ((SELECTED,), True), 'CREATE': ((AUTH, SELECTED), True), 'DELETE': ((AUTH, SELECTED), True), 'DELETEACL': ((AUTH, SELECTED), True), 'EXAMINE': ((AUTH, SELECTED), False), 'EXPUNGE': ((SELECTED,), True), 'FETCH': ((SELECTED,), True), 'GETACL': ((AUTH, SELECTED), True), 'GETANNOTATION':((AUTH, SELECTED), True), 'GETQUOTA': ((AUTH, SELECTED), True), 'GETQUOTAROOT': ((AUTH, SELECTED), True), 'ID': ((NONAUTH, AUTH, SELECTED), True), 'IDLE': ((SELECTED,), False), 'LIST': ((AUTH, SELECTED), True), 'LOGIN': ((NONAUTH,), False), 'LOGOUT': ((NONAUTH, AUTH, LOGOUT, SELECTED), False), 'LSUB': ((AUTH, SELECTED), True), 'MYRIGHTS': ((AUTH, SELECTED), True), 'NAMESPACE': ((AUTH, SELECTED), True), 'NOOP': ((NONAUTH, AUTH, SELECTED), True), 'PARTIAL': ((SELECTED,), True), 'PROXYAUTH': ((AUTH,), False), 'RENAME': ((AUTH, SELECTED), True), 'SEARCH': ((SELECTED,), True), 'SELECT': ((AUTH, SELECTED), False), 'SETACL': ((AUTH, SELECTED), False), 'SETANNOTATION':((AUTH, SELECTED), True), 'SETQUOTA': ((AUTH, SELECTED), False), 'SORT': ((SELECTED,), True), 'STARTTLS': ((NONAUTH,), False), 'STATUS': ((AUTH, SELECTED), True), 'STORE': ((SELECTED,), True), 'SUBSCRIBE': ((AUTH, SELECTED), False), 'THREAD': ((SELECTED,), True), 'UID': ((SELECTED,), True), 'UNSUBSCRIBE': ((AUTH, SELECTED), False), } UID_direct = ('SEARCH', 'SORT', 'THREAD') def Int2AP(num): """string = Int2AP(num) Return 'num' converted to a string using characters from the set 'A'..'P' """ val, a2p = [], 'ABCDEFGHIJKLMNOP' num = int(abs(num)) while num: num, mod = divmod(num, 16) val.insert(0, a2p[mod]) return ''.join(val) class Request(object): """Private class to represent a request awaiting response.""" def __init__(self, parent, name=None, callback=None, cb_arg=None, cb_self=False): self.parent = parent self.name = name self.callback = callback # Function called to process result if not cb_self: self.callback_arg = cb_arg # Optional arg passed to "callback" else: self.callback_arg = (self, cb_arg) # Self reference required in callback arg self.tag = '%s%s' % (parent.tagpre, parent.tagnum) parent.tagnum += 1 self.ready = threading.Event() self.response = None self.aborted = None self.data = None def abort(self, typ, val): self.aborted = (typ, val) self.deliver(None) def get_response(self, exc_fmt=None): self.callback = None if __debug__: self.parent._log(3, '%s:%s.ready.wait' % (self.name, self.tag)) self.ready.wait() if self.aborted is not None: typ, val = self.aborted if exc_fmt is None: exc_fmt = '%s - %%s' % typ raise typ(exc_fmt % str(val)) return self.response def deliver(self, response): if self.callback is not None: self.callback((response, self.callback_arg, self.aborted)) return self.response = response self.ready.set() if __debug__: self.parent._log(3, '%s:%s.ready.set' % (self.name, self.tag)) class IMAP4(object): """Threaded IMAP4 client class. Instantiate with: IMAP4(host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None) host - host's name (default: localhost); port - port number (default: standard IMAP4 port); debug - debug level (default: 0 - no debug); debug_file - debug stream (default: sys.stderr); identifier - thread identifier prefix (default: host); timeout - timeout in seconds when expecting a command response (default: no timeout), debug_buf_lvl - debug level at which buffering is turned off. All IMAP4rev1 commands are supported by methods of the same name. Each command returns a tuple: (type, [data, ...]) where 'type' is usually 'OK' or 'NO', and 'data' is either the text from the tagged response, or untagged results from command. Each 'data' is either a string, or a tuple. If a tuple, then the first part is the header of the response, and the second part contains the data (ie: 'literal' value). Errors raise the exception class .error(""). IMAP4 server errors raise .abort(""), which is a sub-class of 'error'. Mailbox status changes from READ-WRITE to READ-ONLY raise the exception class .readonly(""), which is a sub-class of 'abort'. "error" exceptions imply a program error. "abort" exceptions imply the connection should be reset, and the command re-tried. "readonly" exceptions imply the command should be re-tried. All commands take two optional named arguments: 'callback' and 'cb_arg' If 'callback' is provided then the command is asynchronous, so after the command is queued for transmission, the call returns immediately with the tuple (None, None). The result will be posted by invoking "callback" with one arg, a tuple: callback((result, cb_arg, None)) or, if there was a problem: callback((None, cb_arg, (exception class, reason))) Otherwise the command is synchronous (waits for result). But note that state-changing commands will both block until previous commands have completed, and block subsequent commands until they have finished. All (non-callback) arguments to commands are converted to strings, except for AUTHENTICATE, and the last argument to APPEND which is passed as an IMAP4 literal. If necessary (the string contains any non-printing characters or white-space and isn't enclosed with either parentheses or double or single quotes) each string is quoted. However, the 'password' argument to the LOGIN command is always quoted. If you want to avoid having an argument string quoted (eg: the 'flags' argument to STORE) then enclose the string in parentheses (eg: "(\Deleted)"). If you are using "sequence sets" containing the wildcard character '*', then enclose the argument in single quotes: the quotes will be removed and the resulting string passed unquoted. Note also that you can pass in an argument with a type that doesn't evaluate to 'basestring' (eg: 'bytearray') and it will be converted to a string without quoting. There is one instance variable, 'state', that is useful for tracking whether the client needs to login to the server. If it has the value "AUTH" after instantiating the class, then the connection is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a mailbox changes the state to be "SELECTED", closing a mailbox changes back to "AUTH", and once the client has logged out, the state changes to "LOGOUT" and no further commands may be issued. Note: to use this module, you must read the RFCs pertaining to the IMAP4 protocol, as the semantics of the arguments to each IMAP4 command are left to the invoker, not to mention the results. Also, most IMAP servers implement a sub-set of the commands available here. Note also that you must call logout() to shut down threads before discarding an instance. """ class error(Exception): pass # Logical errors - debug required class abort(error): pass # Service errors - close and retry class readonly(abort): pass # Mailbox status changed to READ-ONLY continuation_cre = re.compile(r'\+( (?P.*))?') literal_cre = re.compile(r'.*{(?P\d+)}$') mapCRLF_cre = re.compile(r'\r\n|\r|\n') # Need to quote "atom-specials" :- # "(" / ")" / "{" / SP / 0x00 - 0x1f / 0x7f / "%" / "*" / DQUOTE / "\" / "]" # so match not the inverse set mustquote_cre = re.compile(r"[^!#$&'+,./0-9:;<=>?@A-Z\[^_`a-z|}~-]") response_code_cre = re.compile(r'\[(?P[A-Z-]+)( (?P[^\]]*))?\]') # sequence_set_cre = re.compile(r"^[0-9]+(:([0-9]+|\*))?(,[0-9]+(:([0-9]+|\*))?)*$") untagged_response_cre = re.compile(r'\* (?P[A-Z-]+)( (?P.*))?') untagged_status_cre = re.compile(r'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?') def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): self.state = NONAUTH # IMAP4 protocol state self.literal = None # A literal argument to a command self.tagged_commands = {} # Tagged commands awaiting response self.untagged_responses = [] # [[typ: [data, ...]], ...] self.mailbox = None # Current mailbox selected self.mailboxes = {} # Untagged responses state per mailbox self.is_readonly = False # READ-ONLY desired state self.idle_rqb = None # Server IDLE Request - see _IdleCont self.idle_timeout = None # Must prod server occasionally self._expecting_data = 0 # Expecting message data self._accumulated_data = [] # Message data accumulated so far self._literal_expected = None # Message data descriptor self.compressor = None # COMPRESS/DEFLATE if not None self.decompressor = None # Create unique tag for this session, # and compile tagged response matcher. self.tagnum = 0 self.tagpre = Int2AP(random.randint(4096, 65535)) self.tagre = re.compile(r'(?P' + self.tagpre + r'\d+) (?P[A-Z]+) (?P.*)') if __debug__: self._init_debug(debug, debug_file, debug_buf_lvl) self.resp_timeout = timeout # Timeout waiting for command response if timeout is not None and timeout < READ_POLL_TIMEOUT: self.read_poll_timeout = timeout else: self.read_poll_timeout = READ_POLL_TIMEOUT self.read_size = READ_SIZE # Open socket to server. self.open(host, port) if __debug__: if debug: self._mesg('connected to %s on port %s' % (self.host, self.port)) # Threading if identifier is not None: self.identifier = identifier else: self.identifier = self.host if self.identifier: self.identifier += ' ' self.Terminate = self.TerminateReader = False self.state_change_free = threading.Event() self.state_change_pending = threading.Lock() self.commands_lock = threading.Lock() self.idle_lock = threading.Lock() self.ouq = Queue.Queue(10) self.inq = Queue.Queue() self.wrth = threading.Thread(target=self._writer) self.wrth.setDaemon(True) self.wrth.start() self.rdth = threading.Thread(target=self._reader) self.rdth.setDaemon(True) self.rdth.start() self.inth = threading.Thread(target=self._handler) self.inth.setDaemon(True) self.inth.start() # Get server welcome message, # request and store CAPABILITY response. try: self.welcome = self._request_push(tag='continuation').get_response('IMAP4 protocol error: %s')[1] if self._get_untagged_response('PREAUTH'): self.state = AUTH if __debug__: self._log(1, 'state => AUTH') elif self._get_untagged_response('OK'): if __debug__: self._log(1, 'state => NONAUTH') else: raise self.error('unrecognised server welcome message: %s' % `self.welcome`) typ, dat = self.capability() if dat == [None]: raise self.error('no CAPABILITY response from server') self.capabilities = tuple(dat[-1].upper().split()) if __debug__: self._log(1, 'CAPABILITY: %r' % (self.capabilities,)) for version in AllowedVersions: if not version in self.capabilities: continue self.PROTOCOL_VERSION = version break else: raise self.error('server not IMAP4 compliant') except: self._close_threads() raise def __getattr__(self, attr): # Allow UPPERCASE variants of IMAP4 command methods. if attr in Commands: return getattr(self, attr.lower()) raise AttributeError("Unknown IMAP4 command: '%s'" % attr) # Overridable methods def open(self, host=None, port=None): """open(host=None, port=None) Setup connection to remote server on "host:port" (default: localhost:standard IMAP4 port). This connection will be used by the routines: read, send, shutdown, socket.""" self.host = self._choose_nonull_or_dflt('', host) self.port = self._choose_nonull_or_dflt(IMAP4_PORT, port) self.sock = self.open_socket() self.read_fd = self.sock.fileno() def open_socket(self): """open_socket() Open socket choosing first address family available.""" msg = (-1, 'could not open socket') for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res try: s = socket.socket(af, socktype, proto) except socket.error, msg: continue try: for i in (0, 1): try: s.connect(sa) break except socket.error, msg: if len(msg.args) < 2 or msg.args[0] != errno.EINTR: raise else: raise socket.error(msg) except socket.error, msg: s.close() continue break else: raise socket.error(msg) return s def ssl_wrap_socket(self): # Allow sending of keep-alive messages - seems to prevent some servers # from closing SSL, leading to deadlocks. self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) try: import ssl if self.ca_certs is not None: cert_reqs = ssl.CERT_REQUIRED else: cert_reqs = ssl.CERT_NONE self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ca_certs=self.ca_certs, cert_reqs=cert_reqs) ssl_exc = ssl.SSLError except ImportError: # No ssl module, and socket.ssl does not allow certificate verification if self.ca_certs is not None: raise socket.sslerror("SSL CA certificates cannot be checked without ssl module") self.sock = socket.ssl(self.sock, self.keyfile, self.certfile) ssl_exc = socket.sslerror if self.cert_verify_cb is not None: cert_err = self.cert_verify_cb(self.sock.getpeercert(), self.host) if cert__err: raise ssl_exc(cert_err) self.read_fd = self.sock.fileno() def start_compressing(self): """start_compressing() Enable deflate compression on the socket (RFC 4978).""" # rfc 1951 - pure DEFLATE, so use -15 for both windows self.decompressor = zlib.decompressobj(-15) self.compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) def read(self, size): """data = read(size) Read at most 'size' bytes from remote.""" if self.decompressor is None: return self.sock.recv(size) if self.decompressor.unconsumed_tail: data = self.decompressor.unconsumed_tail else: data = self.sock.recv(8192) return self.decompressor.decompress(data, size) def send(self, data): """send(data) Send 'data' to remote.""" if self.compressor is not None: data = self.compressor.compress(data) data += self.compressor.flush(zlib.Z_SYNC_FLUSH) self.sock.sendall(data) def shutdown(self): """shutdown() Close I/O established in "open".""" self.sock.close() def socket(self): """socket = socket() Return socket instance used to connect to IMAP4 server.""" return self.sock # Utility methods def enable_compression(self): """enable_compression() Ask the server to start compressing the connection. Should be called from user of this class after instantiation, as in: if 'COMPRESS=DEFLATE' in imapobj.capabilities: imapobj.enable_compression()""" try: typ, dat = self._simple_command('COMPRESS', 'DEFLATE') if typ == 'OK': self.start_compressing() if __debug__: self._log(1, 'Enabled COMPRESS=DEFLATE') finally: self._release_state_change() def pop_untagged_responses(self): """ for typ,data in pop_untagged_responses(): pass Generator for any remaining untagged responses. Returns and removes untagged responses in order of reception. Use at your own risk!""" while self.untagged_responses: self.commands_lock.acquire() try: yield self.untagged_responses.pop(0) finally: self.commands_lock.release() def recent(self, **kw): """(typ, [data]) = recent() Return 'RECENT' responses if any exist, else prompt server for an update using the 'NOOP' command. 'data' is None if no new messages, else list of RECENT responses, most recent last.""" name = 'RECENT' typ, dat = self._untagged_response(None, [None], name) if dat != [None]: return self._deliver_dat(typ, dat, kw) kw['untagged_response'] = name return self.noop(**kw) # Prod server for response def response(self, code, **kw): """(code, [data]) = response(code) Return data for response 'code' if received, or None. Old value for response 'code' is cleared.""" typ, dat = self._untagged_response(code, [None], code.upper()) return self._deliver_dat(typ, dat, kw) # IMAP4 commands def append(self, mailbox, flags, date_time, message, **kw): """(typ, [data]) = append(mailbox, flags, date_time, message) Append message to named mailbox. All args except `message' can be None.""" name = 'APPEND' if not mailbox: mailbox = 'INBOX' if flags: if (flags[0],flags[-1]) != ('(',')'): flags = '(%s)' % flags else: flags = None if date_time: date_time = Time2Internaldate(date_time) else: date_time = None self.literal = self.mapCRLF_cre.sub(CRLF, message) try: return self._simple_command(name, mailbox, flags, date_time, **kw) finally: self._release_state_change() def authenticate(self, mechanism, authobject, **kw): """(typ, [data]) = authenticate(mechanism, authobject) Authenticate command - requires response processing. 'mechanism' specifies which authentication mechanism is to be used - it must appear in .capabilities in the form AUTH=. 'authobject' must be a callable object: data = authobject(response) It will be called to process server continuation responses. It should return data that will be encoded and sent to server. It should return None if the client abort response '*' should be sent instead.""" self.literal = _Authenticator(authobject).process try: typ, dat = self._simple_command('AUTHENTICATE', mechanism.upper()) if typ != 'OK': self._deliver_exc(self.error, dat[-1], kw) self.state = AUTH if __debug__: self._log(1, 'state => AUTH') finally: self._release_state_change() return self._deliver_dat(typ, dat, kw) def capability(self, **kw): """(typ, [data]) = capability() Fetch capabilities list from server.""" name = 'CAPABILITY' kw['untagged_response'] = name return self._simple_command(name, **kw) def check(self, **kw): """(typ, [data]) = check() Checkpoint mailbox on server.""" return self._simple_command('CHECK', **kw) def close(self, **kw): """(typ, [data]) = close() Close currently selected mailbox. Deleted messages are removed from writable mailbox. This is the recommended command before 'LOGOUT'.""" if self.state != 'SELECTED': raise self.error('No mailbox selected.') try: typ, dat = self._simple_command('CLOSE') finally: self.state = AUTH if __debug__: self._log(1, 'state => AUTH') self._release_state_change() return self._deliver_dat(typ, dat, kw) def copy(self, message_set, new_mailbox, **kw): """(typ, [data]) = copy(message_set, new_mailbox) Copy 'message_set' messages onto end of 'new_mailbox'.""" return self._simple_command('COPY', message_set, new_mailbox, **kw) def create(self, mailbox, **kw): """(typ, [data]) = create(mailbox) Create new mailbox.""" return self._simple_command('CREATE', mailbox, **kw) def delete(self, mailbox, **kw): """(typ, [data]) = delete(mailbox) Delete old mailbox.""" return self._simple_command('DELETE', mailbox, **kw) def deleteacl(self, mailbox, who, **kw): """(typ, [data]) = deleteacl(mailbox, who) Delete the ACLs (remove any rights) set for who on mailbox.""" return self._simple_command('DELETEACL', mailbox, who, **kw) def examine(self, mailbox='INBOX', **kw): """(typ, [data]) = examine(mailbox='INBOX', readonly=False) Select a mailbox for READ-ONLY access. (Flushes all untagged responses.) 'data' is count of messages in mailbox ('EXISTS' response). Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so other responses should be obtained via "response('FLAGS')" etc.""" return self.select(mailbox=mailbox, readonly=True, **kw) def expunge(self, **kw): """(typ, [data]) = expunge() Permanently remove deleted items from selected mailbox. Generates 'EXPUNGE' response for each deleted message. 'data' is list of 'EXPUNGE'd message numbers in order received.""" name = 'EXPUNGE' kw['untagged_response'] = name return self._simple_command(name, **kw) def fetch(self, message_set, message_parts, **kw): """(typ, [data, ...]) = fetch(message_set, message_parts) Fetch (parts of) messages. 'message_parts' should be a string of selected parts enclosed in parentheses, eg: "(UID BODY[TEXT])". 'data' are tuples of message part envelope and data, followed by a string containing the trailer.""" name = 'FETCH' kw['untagged_response'] = name return self._simple_command(name, message_set, message_parts, **kw) def getacl(self, mailbox, **kw): """(typ, [data]) = getacl(mailbox) Get the ACLs for a mailbox.""" kw['untagged_response'] = 'ACL' return self._simple_command('GETACL', mailbox, **kw) def getannotation(self, mailbox, entry, attribute, **kw): """(typ, [data]) = getannotation(mailbox, entry, attribute) Retrieve ANNOTATIONs.""" kw['untagged_response'] = 'ANNOTATION' return self._simple_command('GETANNOTATION', mailbox, entry, attribute, **kw) def getquota(self, root, **kw): """(typ, [data]) = getquota(root) Get the quota root's resource usage and limits. (Part of the IMAP4 QUOTA extension defined in rfc2087.)""" kw['untagged_response'] = 'QUOTA' return self._simple_command('GETQUOTA', root, **kw) def getquotaroot(self, mailbox, **kw): # Hmmm, this is non-std! Left for backwards-compatibility, sigh. # NB: usage should have been defined as: # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox) # (typ, [QUOTA responses...]) = response('QUOTA') """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox) Get the list of quota roots for the named mailbox.""" typ, dat = self._simple_command('GETQUOTAROOT', mailbox) typ, quota = self._untagged_response(typ, dat, 'QUOTA') typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT') return self._deliver_dat(typ, [quotaroot, quota], kw) def id(self, *kv_pairs, **kw): """(typ, [data]) = .id(kv_pairs) 'data' is list of ID key value pairs. Request information for problem analysis and determination. The ID extension is defined in RFC 2971. """ name = 'ID' kw['untagged_response'] = name return self._simple_command(name, *kv_pairs, **kw) def idle(self, timeout=None, **kw): """"(typ, [data]) = idle(timeout=None) Put server into IDLE mode until server notifies some change, or 'timeout' (secs) occurs (default: 29 minutes), or another IMAP4 command is scheduled.""" name = 'IDLE' self.literal = _IdleCont(self, timeout).process try: return self._simple_command(name, **kw) finally: self._release_state_change() def list(self, directory='""', pattern='*', **kw): """(typ, [data]) = list(directory='""', pattern='*') List mailbox names in directory matching pattern. 'data' is list of LIST responses. NB: for 'pattern': % matches all except separator ( so LIST "" "%" returns names at root) * matches all (so LIST "" "*" returns whole directory tree from root)""" name = 'LIST' kw['untagged_response'] = name return self._simple_command(name, directory, pattern, **kw) def login(self, user, password, **kw): """(typ, [data]) = login(user, password) Identify client using plaintext password. NB: 'password' will be quoted.""" try: typ, dat = self._simple_command('LOGIN', user, self._quote(password)) if typ != 'OK': self._deliver_exc(self.error, dat[-1], kw) self.state = AUTH if __debug__: self._log(1, 'state => AUTH') finally: self._release_state_change() return self._deliver_dat(typ, dat, kw) def login_cram_md5(self, user, password, **kw): """(typ, [data]) = login_cram_md5(user, password) Force use of CRAM-MD5 authentication.""" self.user, self.password = user, password return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH, **kw) def _CRAM_MD5_AUTH(self, challenge): """Authobject to use with CRAM-MD5 authentication.""" import hmac return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest() def logout(self, **kw): """(typ, [data]) = logout() Shutdown connection to server. Returns server 'BYE' response. NB: You must call this to shut down threads before discarding an instance.""" self.state = LOGOUT if __debug__: self._log(1, 'state => LOGOUT') try: try: typ, dat = self._simple_command('LOGOUT') except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] if __debug__: self._log(1, dat) self._close_threads() finally: self._release_state_change() if __debug__: self._log(1, 'connection closed') bye = self._get_untagged_response('BYE', leave=True) if bye: typ, dat = 'BYE', bye return self._deliver_dat(typ, dat, kw) def lsub(self, directory='""', pattern='*', **kw): """(typ, [data, ...]) = lsub(directory='""', pattern='*') List 'subscribed' mailbox names in directory matching pattern. 'data' are tuples of message part envelope and data.""" name = 'LSUB' kw['untagged_response'] = name return self._simple_command(name, directory, pattern, **kw) def myrights(self, mailbox, **kw): """(typ, [data]) = myrights(mailbox) Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).""" name = 'MYRIGHTS' kw['untagged_response'] = name return self._simple_command(name, mailbox, **kw) def namespace(self, **kw): """(typ, [data, ...]) = namespace() Returns IMAP namespaces ala rfc2342.""" name = 'NAMESPACE' kw['untagged_response'] = name return self._simple_command(name, **kw) def noop(self, **kw): """(typ, [data]) = noop() Send NOOP command.""" if __debug__: self._dump_ur(3) return self._simple_command('NOOP', **kw) def partial(self, message_num, message_part, start, length, **kw): """(typ, [data, ...]) = partial(message_num, message_part, start, length) Fetch truncated part of a message. 'data' is tuple of message part envelope and data. NB: obsolete.""" name = 'PARTIAL' kw['untagged_response'] = 'FETCH' return self._simple_command(name, message_num, message_part, start, length, **kw) def proxyauth(self, user, **kw): """(typ, [data]) = proxyauth(user) Assume authentication as 'user'. (Allows an authorised administrator to proxy into any user's mailbox.)""" try: return self._simple_command('PROXYAUTH', user, **kw) finally: self._release_state_change() def rename(self, oldmailbox, newmailbox, **kw): """(typ, [data]) = rename(oldmailbox, newmailbox) Rename old mailbox name to new.""" return self._simple_command('RENAME', oldmailbox, newmailbox, **kw) def search(self, charset, *criteria, **kw): """(typ, [data]) = search(charset, criterion, ...) Search mailbox for matching messages. 'data' is space separated list of matching message numbers.""" name = 'SEARCH' kw['untagged_response'] = name if charset: return self._simple_command(name, 'CHARSET', charset, *criteria, **kw) return self._simple_command(name, *criteria, **kw) def select(self, mailbox='INBOX', readonly=False, **kw): """(typ, [data]) = select(mailbox='INBOX', readonly=False) Select a mailbox. (Restores any previous untagged responses.) 'data' is count of messages in mailbox ('EXISTS' response). Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so other responses should be obtained via "response('FLAGS')" etc.""" self.commands_lock.acquire() # Save state of old mailbox, restore state for new... self.mailboxes[self.mailbox] = self.untagged_responses self.untagged_responses = self.mailboxes.setdefault(mailbox, []) self.commands_lock.release() self.mailbox = mailbox self.is_readonly = readonly and True or False if readonly: name = 'EXAMINE' else: name = 'SELECT' try: rqb = self._command(name, mailbox) typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) if typ != 'OK': if self.state == SELECTED: self.state = AUTH if __debug__: self._log(1, 'state => AUTH') if typ == 'BAD': self._deliver_exc(self.error, '%s command error: %s %s. Data: %.100s' % (name, typ, dat, mailbox), kw) return self._deliver_dat(typ, dat, kw) self.state = SELECTED if __debug__: self._log(1, 'state => SELECTED') finally: self._release_state_change() if self._get_untagged_response('READ-ONLY', leave=True) and not readonly: if __debug__: self._dump_ur(1) self._deliver_exc(self.readonly, '%s is not writable' % mailbox, kw) typ, dat = self._untagged_response(typ, [None], 'EXISTS') return self._deliver_dat(typ, dat, kw) def setacl(self, mailbox, who, what, **kw): """(typ, [data]) = setacl(mailbox, who, what) Set a mailbox acl.""" try: return self._simple_command('SETACL', mailbox, who, what, **kw) finally: self._release_state_change() def setannotation(self, *args, **kw): """(typ, [data]) = setannotation(mailbox[, entry, attribute]+) Set ANNOTATIONs.""" kw['untagged_response'] = 'ANNOTATION' return self._simple_command('SETANNOTATION', *args, **kw) def setquota(self, root, limits, **kw): """(typ, [data]) = setquota(root, limits) Set the quota root's resource limits.""" kw['untagged_response'] = 'QUOTA' try: return self._simple_command('SETQUOTA', root, limits, **kw) finally: self._release_state_change() def sort(self, sort_criteria, charset, *search_criteria, **kw): """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...) IMAP4rev1 extension SORT command.""" name = 'SORT' if (sort_criteria[0],sort_criteria[-1]) != ('(',')'): sort_criteria = '(%s)' % sort_criteria kw['untagged_response'] = name return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw) def starttls(self, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, **kw): """(typ, [data]) = starttls(keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None) Start TLS negotiation as per RFC 2595.""" name = 'STARTTLS' if name not in self.capabilities: raise self.abort('TLS not supported by server') if hasattr(self, '_tls_established') and self._tls_established: raise self.abort('TLS session already established') # Must now shutdown reader thread after next response, and restart after changing read_fd self.read_size = 1 # Don't consume TLS handshake self.TerminateReader = True try: typ, dat = self._simple_command(name) finally: self._release_state_change() self.rdth.join() self.TerminateReader = False self.read_size = READ_SIZE if typ != 'OK': # Restart reader thread and error self.rdth = threading.Thread(target=self._reader) self.rdth.setDaemon(True) self.rdth.start() raise self.error("Couldn't establish TLS session: %s" % dat) self.keyfile = keyfile self.certfile = certfile self.ca_certs = ca_certs self.cert_verify_cb = cert_verify_cb try: self.ssl_wrap_socket() finally: # Restart reader thread self.rdth = threading.Thread(target=self._reader) self.rdth.setDaemon(True) self.rdth.start() typ, dat = self.capability() if dat == [None]: raise self.error('no CAPABILITY response from server') self.capabilities = tuple(dat[-1].upper().split()) self._tls_established = True typ, dat = self._untagged_response(typ, dat, name) return self._deliver_dat(typ, dat, kw) def status(self, mailbox, names, **kw): """(typ, [data]) = status(mailbox, names) Request named status conditions for mailbox.""" name = 'STATUS' kw['untagged_response'] = name return self._simple_command(name, mailbox, names, **kw) def store(self, message_set, command, flags, **kw): """(typ, [data]) = store(message_set, command, flags) Alters flag dispositions for messages in mailbox.""" if (flags[0],flags[-1]) != ('(',')'): flags = '(%s)' % flags # Avoid quoting the flags kw['untagged_response'] = 'FETCH' return self._simple_command('STORE', message_set, command, flags, **kw) def subscribe(self, mailbox, **kw): """(typ, [data]) = subscribe(mailbox) Subscribe to new mailbox.""" try: return self._simple_command('SUBSCRIBE', mailbox, **kw) finally: self._release_state_change() def thread(self, threading_algorithm, charset, *search_criteria, **kw): """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...) IMAPrev1 extension THREAD command.""" name = 'THREAD' kw['untagged_response'] = name return self._simple_command(name, threading_algorithm, charset, *search_criteria, **kw) def uid(self, command, *args, **kw): """(typ, [data]) = uid(command, arg, ...) Execute "command arg ..." with messages identified by UID, rather than message number. Assumes 'command' is legal in current state. Returns response appropriate to 'command'.""" command = command.upper() if command in UID_direct: resp = command else: resp = 'FETCH' kw['untagged_response'] = resp return self._simple_command('UID', command, *args, **kw) def unsubscribe(self, mailbox, **kw): """(typ, [data]) = unsubscribe(mailbox) Unsubscribe from old mailbox.""" try: return self._simple_command('UNSUBSCRIBE', mailbox, **kw) finally: self._release_state_change() def xatom(self, name, *args, **kw): """(typ, [data]) = xatom(name, arg, ...) Allow simple extension commands notified by server in CAPABILITY response. Assumes extension command 'name' is legal in current state. Returns response appropriate to extension command 'name'.""" name = name.upper() if not name in Commands: Commands[name] = ((self.state,), False) try: return self._simple_command(name, *args, **kw) finally: self._release_state_change() # Internal methods def _append_untagged(self, typ, dat): # Append new 'dat' to end of last untagged response if same 'typ', # else append new response. if dat is None: dat = '' self.commands_lock.acquire() if self.untagged_responses: urn, urd = self.untagged_responses[-1] if urn != typ: urd = None else: urd = None if urd is None: urd = [] self.untagged_responses.append([typ, urd]) urd.append(dat) self.commands_lock.release() if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%s"]' % (typ, len(urd)-1, dat)) def _check_bye(self): bye = self._get_untagged_response('BYE', leave=True) if bye: raise self.abort(bye[-1]) def _checkquote(self, arg): # Must quote command args if "atom-specials" present, # and not already quoted. NB: single quotes are removed. if not isinstance(arg, basestring): return arg if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')): return arg if len(arg) >= 2 and (arg[0],arg[-1]) in (("'","'"),): return arg[1:-1] if arg and self.mustquote_cre.search(arg) is None: return arg return self._quote(arg) def _choose_nonull_or_dflt(self, dflt, *args): dflttyp = type(dflt) for arg in args: if arg is not None: if type(arg) is dflttyp: return arg if __debug__: self._log(1, 'bad arg type is %s, expecting %s' % (type(arg), dflttyp)) return dflt def _command(self, name, *args, **kw): if Commands[name][CMD_VAL_ASYNC]: cmdtyp = 'async' else: cmdtyp = 'sync' if __debug__: self._log(1, '[%s] %s %s' % (cmdtyp, name, args)) if __debug__: self._log(3, 'state_change_pending.acquire') self.state_change_pending.acquire() self._end_idle() if cmdtyp == 'async': self.state_change_pending.release() if __debug__: self._log(3, 'state_change_pending.release') else: # Need to wait for all async commands to complete self._check_bye() self.commands_lock.acquire() if self.tagged_commands: self.state_change_free.clear() need_event = True else: need_event = False self.commands_lock.release() if need_event: if __debug__: self._log(3, 'sync command %s waiting for empty commands Q' % name) self.state_change_free.wait() if __debug__: self._log(3, 'sync command %s proceeding' % name) if self.state not in Commands[name][CMD_VAL_STATES]: self.literal = None raise self.error('command %s illegal in state %s' % (name, self.state)) self._check_bye() for typ in ('OK', 'NO', 'BAD'): self._get_untagged_response(typ) if self._get_untagged_response('READ-ONLY', leave=True) and not self.is_readonly: self.literal = None raise self.readonly('mailbox status changed to READ-ONLY') if self.Terminate: raise self.abort('connection closed') rqb = self._request_push(name=name, **kw) data = '%s %s' % (rqb.tag, name) for arg in args: if arg is None: continue data = '%s %s' % (data, self._checkquote(arg)) literal = self.literal if literal is not None: self.literal = None if isinstance(literal, basestring): literator = None data = '%s {%s}' % (data, len(literal)) else: literator = literal if __debug__: self._log(4, 'data=%s' % data) rqb.data = '%s%s' % (data, CRLF) if literal is None: self.ouq.put(rqb) return rqb # Must setup continuation expectancy *before* ouq.put crqb = self._request_push(tag='continuation') self.ouq.put(rqb) while True: # Wait for continuation response ok, data = crqb.get_response('command: %s => %%s' % name) if __debug__: self._log(4, 'continuation => %s, %s' % (ok, data)) # NO/BAD response? if not ok: break # Send literal if literator is not None: literal = literator(data, rqb) if literal is None: break if literator is not None: # Need new request for next continuation response crqb = self._request_push(tag='continuation') if __debug__: self._log(4, 'write literal size %s' % len(literal)) crqb.data = '%s%s' % (literal, CRLF) self.ouq.put(crqb) if literator is None: break return rqb def _command_complete(self, rqb, kw): # Called for non-callback commands typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) self._check_bye() if typ == 'BAD': if __debug__: self._print_log() raise self.error('%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data)) if 'untagged_response' in kw: return self._untagged_response(typ, dat, kw['untagged_response']) return typ, dat def _command_completer(self, (response, cb_arg, error)): # Called for callback commands rqb, kw = cb_arg rqb.callback = kw['callback'] rqb.callback_arg = kw.get('cb_arg') if error is not None: if __debug__: self._print_log() typ, val = error rqb.abort(typ, val) return bye = self._get_untagged_response('BYE', leave=True) if bye: rqb.abort(self.abort, bye[-1]) return typ, dat = response if typ == 'BAD': if __debug__: self._print_log() rqb.abort(self.error, '%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data)) return if 'untagged_response' in kw: response = self._untagged_response(typ, dat, kw['untagged_response']) rqb.deliver(response) def _deliver_dat(self, typ, dat, kw): if 'callback' in kw: kw['callback'](((typ, dat), kw.get('cb_arg'), None)) return typ, dat def _deliver_exc(self, exc, dat, kw): if 'callback' in kw: kw['callback']((None, kw.get('cb_arg'), (exc, dat))) raise exc(dat) def _end_idle(self): self.idle_lock.acquire() irqb = self.idle_rqb if irqb is None: self.idle_lock.release() return self.idle_rqb = None self.idle_timeout = None self.idle_lock.release() irqb.data = 'DONE%s' % CRLF self.ouq.put(irqb) if __debug__: self._log(2, 'server IDLE finished') def _get_untagged_response(self, name, leave=False): self.commands_lock.acquire() for i, (typ, dat) in enumerate(self.untagged_responses): if typ == name: if not leave: del self.untagged_responses[i] self.commands_lock.release() if __debug__: self._log(5, '_get_untagged_response(%s) => %s' % (name, dat)) return dat self.commands_lock.release() return None def _match(self, cre, s): # Run compiled regular expression 'cre' match method on 's'. # Save result, return success. self.mo = cre.match(s) return self.mo is not None def _put_response(self, resp): if self._expecting_data > 0: rlen = len(resp) dlen = min(self._expecting_data, rlen) self._expecting_data -= dlen if rlen <= dlen: self._accumulated_data.append(resp) return self._accumulated_data.append(resp[:dlen]) resp = resp[dlen:] if self._accumulated_data: typ, dat = self._literal_expected self._append_untagged(typ, (dat, ''.join(self._accumulated_data))) self._accumulated_data = [] # Protocol mandates all lines terminated by CRLF resp = resp[:-2] if 'continuation' in self.tagged_commands: continuation_expected = True else: continuation_expected = False if self._literal_expected is not None: dat = resp if self._match(self.literal_cre, dat): self._literal_expected[1] = dat self._expecting_data = int(self.mo.group('size')) if __debug__: self._log(4, 'expecting literal size %s' % self._expecting_data) return typ = self._literal_expected[0] self._literal_expected = None self._append_untagged(typ, dat) # Tail if __debug__: self._log(4, 'literal completed') else: # Command completion response? if self._match(self.tagre, resp): tag = self.mo.group('tag') typ = self.mo.group('type') dat = self.mo.group('data') if not tag in self.tagged_commands: if __debug__: self._log(1, 'unexpected tagged response: %s' % resp) else: self._request_pop(tag, (typ, [dat])) else: dat2 = None # '*' (untagged) responses? if not self._match(self.untagged_response_cre, resp): if self._match(self.untagged_status_cre, resp): dat2 = self.mo.group('data2') if self.mo is None: # Only other possibility is '+' (continuation) response... if self._match(self.continuation_cre, resp): if not continuation_expected: if __debug__: self._log(1, "unexpected continuation response: '%s'" % resp) return self._request_pop('continuation', (True, self.mo.group('data'))) return if __debug__: self._log(1, "unexpected response: '%s'" % resp) return typ = self.mo.group('type') dat = self.mo.group('data') if dat is None: dat = '' # Null untagged response if dat2: dat = dat + ' ' + dat2 # Is there a literal to come? if self._match(self.literal_cre, dat): self._expecting_data = int(self.mo.group('size')) if __debug__: self._log(4, 'read literal size %s' % self._expecting_data) self._literal_expected = [typ, dat] return self._append_untagged(typ, dat) if typ != 'OK': # NO, BYE, IDLE self._end_idle() # Bracketed response information? if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): self._append_untagged(self.mo.group('type'), self.mo.group('data')) # Command waiting for aborted continuation response? if continuation_expected: self._request_pop('continuation', (False, resp)) # Bad news? if typ in ('NO', 'BAD', 'BYE'): if typ == 'BYE': self.Terminate = True if __debug__: self._log(1, '%s response: %s' % (typ, dat)) def _quote(self, arg): return '"%s"' % arg.replace('\\', '\\\\').replace('"', '\\"') def _release_state_change(self): if self.state_change_pending.locked(): self.state_change_pending.release() if __debug__: self._log(3, 'state_change_pending.release') def _request_pop(self, name, data): self.commands_lock.acquire() rqb = self.tagged_commands.pop(name) if not self.tagged_commands: if __debug__: self._log(3, 'state_change_free.set') self.state_change_free.set() self.commands_lock.release() if __debug__: self._log(4, '_request_pop(%s, %s) = %s' % (name, data, rqb.tag)) rqb.deliver(data) def _request_push(self, tag=None, name=None, **kw): self.commands_lock.acquire() rqb = Request(self, name=name, **kw) if tag is None: tag = rqb.tag self.tagged_commands[tag] = rqb self.commands_lock.release() if __debug__: self._log(4, '_request_push(%s, %s, %s) = %s' % (tag, name, `kw`, rqb.tag)) return rqb def _simple_command(self, name, *args, **kw): if 'callback' in kw: self._command(name, *args, callback=self._command_completer, cb_arg=kw, cb_self=True) return (None, None) return self._command_complete(self._command(name, *args), kw) def _untagged_response(self, typ, dat, name): if typ == 'NO': return typ, dat data = self._get_untagged_response(name) if not data: return typ, [None] while True: dat = self._get_untagged_response(name) if not dat: break data += dat if __debug__: self._log(4, '_untagged_response(%s, ?, %s) => %s' % (typ, name, data)) return typ, data # Threads def _close_threads(self): if __debug__: self._log(1, '_close_threads') self.ouq.put(None) self.wrth.join() if __debug__: self._log(1, 'call shutdown') self.shutdown() self.rdth.join() self.inth.join() def _handler(self): resp_timeout = self.resp_timeout threading.currentThread().setName(self.identifier + 'handler') time.sleep(0.1) # Don't start handling before main thread ready if __debug__: self._log(1, 'starting') typ, val = self.abort, 'connection terminated' while not self.Terminate: try: if self.idle_timeout is not None: timeout = self.idle_timeout - time.time() if timeout <= 0: timeout = 1 if __debug__: if self.idle_rqb is not None: self._log(5, 'server IDLING, timeout=%.2f' % timeout) else: timeout = resp_timeout line = self.inq.get(True, timeout) except Queue.Empty: if self.idle_rqb is None: if resp_timeout is not None and self.tagged_commands: if __debug__: self._log(1, 'response timeout') typ, val = self.abort, 'no response after %s secs' % resp_timeout break continue if self.idle_timeout > time.time(): continue if __debug__: self._log(2, 'server IDLE timedout') line = IDLE_TIMEOUT_RESPONSE if line is None: if __debug__: self._log(1, 'inq None - terminating') break if not isinstance(line, basestring): typ, val = line break try: self._put_response(line) except: typ, val = self.error, 'program error: %s - %s' % sys.exc_info()[:2] break self.Terminate = True if __debug__: self._log(1, 'terminating: %s' % `val`) while not self.ouq.empty(): try: self.ouq.get_nowait().abort(typ, val) except Queue.Empty: break self.ouq.put(None) self.commands_lock.acquire() for name in self.tagged_commands.keys(): rqb = self.tagged_commands.pop(name) rqb.abort(typ, val) self.state_change_free.set() self.commands_lock.release() if __debug__: self._log(3, 'state_change_free.set') if __debug__: self._log(1, 'finished') if hasattr(select_module, "poll"): def _reader(self): threading.currentThread().setName(self.identifier + 'reader') if __debug__: self._log(1, 'starting using poll') def poll_error(state): PollErrors = { select.POLLERR: 'Error', select.POLLHUP: 'Hang up', select.POLLNVAL: 'Invalid request: descriptor not open', } return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)]) line_part = '' poll = select.poll() poll.register(self.read_fd, select.POLLIN) rxzero = 0 terminate = False read_poll_timeout = self.read_poll_timeout * 1000 # poll() timeout is in millisecs while not (terminate or self.Terminate): if self.state == LOGOUT: timeout = 1 else: timeout = read_poll_timeout try: r = poll.poll(timeout) if __debug__: self._log(5, 'poll => %s' % `r`) if not r: continue # Timeout fd,state = r[0] if state & select.POLLIN: data = self.read(self.read_size) # Drain ssl buffer if present start = 0 dlen = len(data) if __debug__: self._log(5, 'rcvd %s' % dlen) if dlen == 0: rxzero += 1 if rxzero > 5: raise IOError("Too many read 0") time.sleep(0.1) else: rxzero = 0 while True: stop = data.find('\n', start) if stop < 0: line_part += data[start:] break stop += 1 line_part, start, line = \ '', stop, line_part + data[start:stop] if __debug__: self._log(4, '< %s' % line) self.inq.put(line) if self.TerminateReader: terminate = True if state & ~(select.POLLIN): raise IOError(poll_error(state)) except: reason = 'socket error: %s - %s' % sys.exc_info()[:2] if __debug__: if not self.Terminate: self._print_log() if self.debug: self.debug += 4 # Output all self._log(1, reason) self.inq.put((self.abort, reason)) break poll.unregister(self.read_fd) if __debug__: self._log(1, 'finished') else: # No "poll" - use select() def _reader(self): threading.currentThread().setName(self.identifier + 'reader') if __debug__: self._log(1, 'starting using select') line_part = '' rxzero = 0 terminate = False while not (terminate or self.Terminate): if self.state == LOGOUT: timeout = 1 else: timeout = self.read_poll_timeout try: r,w,e = select.select([self.read_fd], [], [], timeout) if __debug__: self._log(5, 'select => %s, %s, %s' % (r,w,e)) if not r: # Timeout continue data = self.read(self.read_size) # Drain ssl buffer if present start = 0 dlen = len(data) if __debug__: self._log(5, 'rcvd %s' % dlen) if dlen == 0: rxzero += 1 if rxzero > 5: raise IOError("Too many read 0") time.sleep(0.1) else: rxzero = 0 while True: stop = data.find('\n', start) if stop < 0: line_part += data[start:] break stop += 1 line_part, start, line = \ '', stop, line_part + data[start:stop] if __debug__: self._log(4, '< %s' % line) self.inq.put(line) if self.TerminateReader: terminate = True except: reason = 'socket error: %s - %s' % sys.exc_info()[:2] if __debug__: if not self.Terminate: self._print_log() if self.debug: self.debug += 4 # Output all self._log(1, reason) self.inq.put((self.abort, reason)) break if __debug__: self._log(1, 'finished') def _writer(self): threading.currentThread().setName(self.identifier + 'writer') if __debug__: self._log(1, 'starting') reason = 'Terminated' while not self.Terminate: rqb = self.ouq.get() if rqb is None: break # Outq flushed try: self.send(rqb.data) if __debug__: self._log(4, '> %s' % rqb.data) except: reason = 'socket error: %s - %s' % sys.exc_info()[:2] if __debug__: if not self.Terminate: self._print_log() if self.debug: self.debug += 4 # Output all self._log(1, reason) rqb.abort(self.abort, reason) break self.inq.put((self.abort, reason)) if __debug__: self._log(1, 'finished') # Debugging if __debug__: def _init_debug(self, debug=None, debug_file=None, debug_buf_lvl=None): self.debug = self._choose_nonull_or_dflt(0, debug, Debug) self.debug_file = self._choose_nonull_or_dflt(sys.stderr, debug_file) self.debug_buf_lvl = self._choose_nonull_or_dflt(DFLT_DEBUG_BUF_LVL, debug_buf_lvl) self.debug_lock = threading.Lock() self._cmd_log_len = 20 self._cmd_log_idx = 0 self._cmd_log = {} # Last `_cmd_log_len' interactions if self.debug: self._mesg('imaplib2 version %s' % __version__) self._mesg('imaplib2 debug level %s, buffer level %s' % (self.debug, self.debug_buf_lvl)) def _dump_ur(self, lvl): if lvl > self.debug: return l = self.untagged_responses if not l: return t = '\n\t\t' l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l) self.debug_lock.acquire() self._mesg('untagged responses dump:%s%s' % (t, t.join(l))) self.debug_lock.release() def _log(self, lvl, line): if lvl > self.debug: return if line[-2:] == CRLF: line = line[:-2] + '\\r\\n' tn = threading.currentThread().getName() if lvl <= 1 or self.debug > self.debug_buf_lvl: self.debug_lock.acquire() self._mesg(line, tn) self.debug_lock.release() if lvl != 1: return # Keep log of last `_cmd_log_len' interactions for debugging. self.debug_lock.acquire() self._cmd_log[self._cmd_log_idx] = (line, tn, time.time()) self._cmd_log_idx += 1 if self._cmd_log_idx >= self._cmd_log_len: self._cmd_log_idx = 0 self.debug_lock.release() def _mesg(self, s, tn=None, secs=None): if secs is None: secs = time.time() if tn is None: tn = threading.currentThread().getName() tm = time.strftime('%M:%S', time.localtime(secs)) try: self.debug_file.write(' %s.%02d %s %s\n' % (tm, (secs*100)%100, tn, s)) self.debug_file.flush() finally: pass def _print_log(self): self.debug_lock.acquire() i, n = self._cmd_log_idx, self._cmd_log_len if n: self._mesg('last %d log messages:' % n) while n: try: self._mesg(*self._cmd_log[i]) except: pass i += 1 if i >= self._cmd_log_len: i = 0 n -= 1 self.debug_lock.release() class IMAP4_SSL(IMAP4): """IMAP4 client class over SSL connection Instantiate with: IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None, identifier=None, timeout=None) host - host's name (default: localhost); port - port number (default: standard IMAP4 SSL port); keyfile - PEM formatted file that contains your private key (default: None); certfile - PEM formatted certificate chain file (default: None); ca_certs - PEM formatted certificate chain file used to validate server certificates (default: None); cert_verify_cb - function to verify authenticity of server certificates (default: None); debug - debug level (default: 0 - no debug); debug_file - debug stream (default: sys.stderr); identifier - thread identifier prefix (default: host); timeout - timeout in seconds when expecting a command response. debug_buf_lvl - debug level at which buffering is turned off. For more documentation see the docstring of the parent class IMAP4. """ def __init__(self, host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): self.keyfile = keyfile self.certfile = certfile self.ca_certs = ca_certs self.cert_verify_cb = cert_verify_cb IMAP4.__init__(self, host, port, debug, debug_file, identifier, timeout, debug_buf_lvl) def open(self, host=None, port=None): """open(host=None, port=None) Setup secure connection to remote server on "host:port" (default: localhost:standard IMAP4 SSL port). This connection will be used by the routines: read, send, shutdown, socket, ssl.""" self.host = self._choose_nonull_or_dflt('', host) self.port = self._choose_nonull_or_dflt(IMAP4_SSL_PORT, port) self.sock = self.open_socket() self.ssl_wrap_socket() def read(self, size): """data = read(size) Read at most 'size' bytes from remote.""" if self.decompressor is None: return self.sock.read(size) if self.decompressor.unconsumed_tail: data = self.decompressor.unconsumed_tail else: data = self.sock.read(8192) return self.decompressor.decompress(data, size) def send(self, data): """send(data) Send 'data' to remote.""" if self.compressor is not None: data = self.compressor.compress(data) data += self.compressor.flush(zlib.Z_SYNC_FLUSH) if hasattr(self.sock, "sendall"): self.sock.sendall(data) else: bytes = len(data) while bytes > 0: sent = self.sock.write(data) if sent == bytes: break # avoid copy data = data[sent:] bytes = bytes - sent def ssl(self): """ssl = ssl() Return socket.ssl instance used to communicate with the IMAP4 server.""" return self.sock class IMAP4_stream(IMAP4): """IMAP4 client class over a stream Instantiate with: IMAP4_stream(command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None) command - string that can be passed to subprocess.Popen(); debug - debug level (default: 0 - no debug); debug_file - debug stream (default: sys.stderr); identifier - thread identifier prefix (default: host); timeout - timeout in seconds when expecting a command response. debug_buf_lvl - debug level at which buffering is turned off. For more documentation see the docstring of the parent class IMAP4. """ def __init__(self, command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): self.command = command self.host = command self.port = None self.sock = None self.writefile, self.readfile = None, None self.read_fd = None IMAP4.__init__(self, None, None, debug, debug_file, identifier, timeout, debug_buf_lvl) def open(self, host=None, port=None): """open(host=None, port=None) Setup a stream connection via 'self.command'. This connection will be used by the routines: read, send, shutdown, socket.""" from subprocess import Popen, PIPE self._P = Popen(self.command, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True) self.writefile, self.readfile = self._P.stdin, self._P.stdout self.read_fd = self.readfile.fileno() def read(self, size): """Read 'size' bytes from remote.""" if self.decompressor is None: return os.read(self.read_fd, size) if self.decompressor.unconsumed_tail: data = self.decompressor.unconsumed_tail else: data = os.read(self.read_fd, 8192) return self.decompressor.decompress(data, size) def send(self, data): """Send data to remote.""" if self.compressor is not None: data = self.compressor.compress(data) data += self.compressor.flush(zlib.Z_SYNC_FLUSH) self.writefile.write(data) self.writefile.flush() def shutdown(self): """Close I/O established in "open".""" self.readfile.close() self.writefile.close() class _Authenticator(object): """Private class to provide en/de-coding for base64 authentication conversation.""" def __init__(self, mechinst): self.mech = mechinst # Callable object to provide/process data def process(self, data, rqb): ret = self.mech(self.decode(data)) if ret is None: return '*' # Abort conversation return self.encode(ret) def encode(self, inp): # # Invoke binascii.b2a_base64 iteratively with # short even length buffers, strip the trailing # line feed from the result and append. "Even" # means a number that factors to both 6 and 8, # so when it gets to the end of the 8-bit input # there's no partial 6-bit output. # oup = '' while inp: if len(inp) > 48: t = inp[:48] inp = inp[48:] else: t = inp inp = '' e = binascii.b2a_base64(t) if e: oup = oup + e[:-1] return oup def decode(self, inp): if not inp: return '' return binascii.a2b_base64(inp) class _IdleCont(object): """When process is called, server is in IDLE state and will send asynchronous changes.""" def __init__(self, parent, timeout): self.parent = parent self.timeout = parent._choose_nonull_or_dflt(IDLE_TIMEOUT, timeout) self.parent.idle_timeout = self.timeout + time.time() def process(self, data, rqb): self.parent.idle_lock.acquire() self.parent.idle_rqb = rqb self.parent.idle_timeout = self.timeout + time.time() self.parent.idle_lock.release() if __debug__: self.parent._log(2, 'server IDLE started, timeout in %.2f secs' % self.timeout) return None MonthNames = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] Mon2num = dict(zip((x.encode() for x in MonthNames[1:]), range(1, 13))) InternalDate = re.compile(r'.*INTERNALDATE "' r'(?P[ 0123][0-9])-(?P[A-Z][a-z][a-z])-(?P[0-9][0-9][0-9][0-9])' r' (?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])' r' (?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])' r'"') def Internaldate2Time(resp): """time_tuple = Internaldate2Time(resp) Convert IMAP4 INTERNALDATE to UT.""" mo = InternalDate.match(resp) if not mo: return None mon = Mon2num[mo.group('mon')] zonen = mo.group('zonen') day = int(mo.group('day')) year = int(mo.group('year')) hour = int(mo.group('hour')) min = int(mo.group('min')) sec = int(mo.group('sec')) zoneh = int(mo.group('zoneh')) zonem = int(mo.group('zonem')) # INTERNALDATE timezone must be subtracted to get UT zone = (zoneh*60 + zonem)*60 if zonen == '-': zone = -zone tt = (year, mon, day, hour, min, sec, -1, -1, -1) utc = time.mktime(tt) # Following is necessary because the time module has no 'mkgmtime'. # 'mktime' assumes arg in local timezone, so adds timezone/altzone. lt = time.localtime(utc) if time.daylight and lt[-1]: zone = zone + time.altzone else: zone = zone + time.timezone return time.localtime(utc - zone) Internaldate2tuple = Internaldate2Time # (Backward compatible) def Time2Internaldate(date_time): """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time) Convert 'date_time' to IMAP4 INTERNALDATE representation.""" if isinstance(date_time, (int, float)): tt = time.localtime(date_time) elif isinstance(date_time, (tuple, time.struct_time)): tt = date_time elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): return date_time # Assume in correct format else: raise ValueError("date_time not of a known type") if time.daylight and tt[-1]: zone = -time.altzone else: zone = -time.timezone return ('"%2d-%s-%04d %02d:%02d:%02d %+03d%02d"' % ((tt[2], MonthNames[tt[1]], tt[0]) + tt[3:6] + divmod(zone//60, 60))) FLAGS_cre = re.compile(r'.*FLAGS \((?P[^\)]*)\)') def ParseFlags(resp): """('flag', ...) = ParseFlags(line) Convert IMAP4 flags response to python tuple.""" mo = FLAGS_cre.match(resp) if not mo: return () return tuple(mo.group('flags').split()) if __name__ == '__main__': # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]', # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"' # or as 'python imaplib2.py -l "keyfile[:certfile]" [IMAP4_SSL_server_hostname]' import getopt, getpass try: optlist, args = getopt.getopt(sys.argv[1:], 'd:l:s:p:') except getopt.error, val: optlist, args = (), () debug, debug_buf_lvl, port, stream_command, keyfile, certfile = (None,)*6 for opt,val in optlist: if opt == '-d': debug = int(val) debug_buf_lvl = debug - 1 elif opt == '-l': try: keyfile,certfile = val.split(':') except ValueError: keyfile,certfile = val,val elif opt == '-p': port = int(val) elif opt == '-s': stream_command = val if not args: args = (stream_command,) if not args: args = ('',) if not port: port = (keyfile is not None) and IMAP4_SSL_PORT or IMAP4_PORT host = args[0] USER = getpass.getuser() data = open(os.path.exists("test.data") and "test.data" or __file__).read(1000) test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \ % {'user':USER, 'lf':'\n', 'data':data} test_seq1 = [ ('list', ('""', '%')), ('create', ('/tmp/imaplib2_test.0',)), ('rename', ('/tmp/imaplib2_test.0', '/tmp/imaplib2_test.1')), ('CREATE', ('/tmp/imaplib2_test.2',)), ('append', ('/tmp/imaplib2_test.2', None, None, test_mesg)), ('list', ('/tmp', 'imaplib2_test*')), ('select', ('/tmp/imaplib2_test.2',)), ('search', (None, 'SUBJECT', 'IMAP4 test')), ('fetch', ("'1:*'", '(FLAGS INTERNALDATE RFC822)')), ('store', ('1', 'FLAGS', '(\Deleted)')), ('namespace', ()), ('expunge', ()), ('recent', ()), ('close', ()), ] test_seq2 = ( ('select', ()), ('response',('UIDVALIDITY',)), ('response', ('EXISTS',)), ('append', (None, None, None, test_mesg)), ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')), ('uid', ('SEARCH', 'ALL')), ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')), ('recent', ()), ) AsyncError = None def responder((response, cb_arg, error)): global AsyncError cmd, args = cb_arg if error is not None: AsyncError = error M._log(0, '[cb] ERROR %s %.100s => %s' % (cmd, args, error)) return typ, dat = response M._log(0, '[cb] %s %.100s => %s %.100s' % (cmd, args, typ, dat)) if typ == 'NO': AsyncError = (Exception, dat[0]) def run(cmd, args, cb=True): if AsyncError: M._log(1, 'AsyncError') M.logout() typ, val = AsyncError raise typ(val) if not M.debug: M._log(0, '%s %.100s' % (cmd, args)) try: if cb: typ, dat = getattr(M, cmd)(callback=responder, cb_arg=(cmd, args), *args) M._log(1, '%s %.100s => %s %.100s' % (cmd, args, typ, dat)) else: typ, dat = getattr(M, cmd)(*args) M._log(1, '%s %.100s => %s %.100s' % (cmd, args, typ, dat)) except: M._log(1, '%s - %s' % sys.exc_info()[:2]) M.logout() raise if typ == 'NO': M._log(1, 'NO') M.logout() raise Exception(dat[0]) return dat try: threading.currentThread().setName('main') if keyfile is not None: if not keyfile: keyfile = None if not certfile: certfile = None M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl) elif stream_command: M = IMAP4_stream(stream_command, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl) else: M = IMAP4(host=host, port=port, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl) if M.state != 'AUTH': # Login needed PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost")) test_seq1.insert(0, ('login', (USER, PASSWD))) M._log(0, 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) if 'COMPRESS=DEFLATE' in M.capabilities: M.enable_compression() for cmd,args in test_seq1: run(cmd, args) for ml in run('list', ('/tmp/', 'imaplib2_test%'), cb=False): mo = re.match(r'.*"([^"]+)"$', ml) if mo: path = mo.group(1) else: path = ml.split()[-1] run('delete', (path,)) for cmd,args in test_seq2: if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')): run(cmd, args) continue dat = run(cmd, args, cb=False) uid = dat[-1].split() if not uid: continue run('uid', ('FETCH', uid[-1], '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) run('uid', ('STORE', uid[-1], 'FLAGS', '(\Deleted)')) run('expunge', ()) if 'IDLE' in M.capabilities: run('idle', (2,), cb=False) run('idle', (99,), cb=True) # Asynchronous, to test interruption of 'idle' by 'noop' time.sleep(1) run('noop', (), cb=False) run('logout', (), cb=False) if debug: M._mesg('') M._print_log() M._mesg('') M._mesg('unused untagged responses in order, most recent last:') for typ,dat in M.pop_untagged_responses(): M._mesg('\t%s %s' % (typ, dat)) print 'All tests OK.' except: print 'Tests failed.' if not debug: print ''' If you would like to see debugging output, try: %s -d5 ''' % sys.argv[0] raise mailnag-1.2.1/Mailnag/common/plugins.py000066400000000000000000000153341272232533200200250ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # plugins.py # # Copyright 2013, 2014 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import os import imp import inspect import logging from Mailnag.common.config import cfg_folder from Mailnag.common.dist_cfg import LIB_DIR PLUGIN_LIB_PATH = os.path.join(LIB_DIR, 'plugins') PLUGIN_USER_PATH = os.path.join(cfg_folder, 'plugins') PLUGIN_PATHS = [ PLUGIN_LIB_PATH, PLUGIN_USER_PATH ] # # All known hook types. # # TODO : make this class an enum # when Mailnag is ported to python3 class HookTypes: # func signature: # IN: List of loaded accounts # OUT: None ACCOUNTS_LOADED = 'accounts-loaded' # func signature: # IN: None # OUT: None MAIL_CHECK = 'mail-check' # func signature: # IN: new mails, all mails # OUT: None MAILS_ADDED = 'mails-added' # func signature: # IN: remaining mails # OUT: None MAILS_REMOVED = 'mails-removed' # func signature: # IN: all mails # OUT: filtered mails FILTER_MAILS = 'filter-mails' # # Registry class for plugin hooks. # # Registered hook functions must not block the mailnag daemon. # Hook functions with an execution time > 1s should be # implemented non-blocking (i. e. asynchronously). class HookRegistry: def __init__(self): self._hooks = { HookTypes.ACCOUNTS_LOADED : [], HookTypes.MAIL_CHECK : [], HookTypes.MAILS_ADDED : [], HookTypes.MAILS_REMOVED : [], HookTypes.FILTER_MAILS : [] } def register_hook_func(self, hooktype, func): self._hooks[hooktype].append(func) def unregister_hook_func(self, hooktype, func): self._hooks[hooktype].remove(func) def get_hook_funcs(self, hooktype): return self._hooks[hooktype] # Abstract base class for a MailnagController instance # passed to plugins. class MailnagController: # Returns a HookRegistry object. def get_hooks(self): pass # Shuts down the Mailnag process. # May throw an InvalidOperationException. def shutdown(self): pass # Enforces a manual mail check. # May throw an InvalidOperationException. def check_for_mails(self): pass # Marks the mail with specified mail_id as read. # May throw an InvalidOperationException. def mark_mail_as_read(self, mail_id): pass # # Mailnag Plugin base class # class Plugin: def __init__(self): # Plugins shouldn't do anything in the constructor. # They are expected to start living if they are actually # enabled (i.e. in the enable() method). # Plugin data isn't enabled yet and call to methods like # get_mailnag_controller() or get_config(). pass # # Abstract methods, # to be overriden by derived plugin types. # def enable(self): # Plugins are expected to # register all hooks here. raise NotImplementedError def disable(self): # Plugins are expected to # unregister all hooks here, # free all allocated resources, # and terminate threads (if any). raise NotImplementedError def get_manifest(self): # Plugins are expected to # return a tuple of the following form: # (name, description, version, author, mandatory). raise NotImplementedError def get_default_config(self): # Plugins are expected to return a # dictionary with default values. raise NotImplementedError def has_config_ui(self): # Plugins are expected to return True if # they provide a configuration widget, # otherwise they must return False. raise NotImplementedError def get_config_ui(self): # Plugins are expected to # return a GTK widget here. # Return None if the plugin # does not need a config widget. raise NotImplementedError def load_ui_from_config(self, config_ui): # Plugins are expected to # load their config values (get_config()) # in the widget returned by get_config_ui(). raise NotImplementedError def save_ui_to_config(self, config_ui): # Plugins are expected to # save the config values of the widget # returned by get_config_ui() to their config # (get_config()). raise NotImplementedError # # Public methods # def init(self, modname, cfg, mailnag_controller): config = {} # try to load plugin config if cfg.has_section(modname): for name, value in cfg.items(modname): config[name] = value # sync with default config default_config = self.get_default_config() for k, v in default_config.iteritems(): if not config.has_key(k): config[k] = v self._modname = modname self._config = config self._mailnag_controller = mailnag_controller def get_name(self): name = self.get_manifest()[0] return name def get_modname(self): return self._modname def get_config(self): return self._config # # Protected methods # def get_mailnag_controller(self): return self._mailnag_controller # # Static methods # # Note : Plugin instances do not own # a reference to MailnagController object # when instantiated in *config mode*. @staticmethod def load_plugins(cfg, mailnag_controller = None, filter_names = None): plugins = [] plugin_types = Plugin._load_plugin_types() for modname, t in plugin_types: try: if (filter_names == None) or (modname in filter_names): p = t() p.init(modname, cfg, mailnag_controller) plugins.append(p) except: logging.exception("Failed to instantiate plugin '%s'" % modname) return plugins @staticmethod def _load_plugin_types(): plugin_types = [] for path in PLUGIN_PATHS: if not os.path.exists(path): continue for f in os.listdir(path): mod = None modname, ext = os.path.splitext(f) try: if ext.lower() == '.py': if not os.path.exists(os.path.join(path, modname + '.pyc')): mod = imp.load_source(modname, os.path.join(path, f)) elif ext.lower() == '.pyc': mod = imp.load_compiled(modname, os.path.join(path, f)) if mod != None: for t in dir(mod): t = getattr(mod, t) if inspect.isclass(t) and \ (inspect.getmodule(t) == mod) and \ issubclass(t, Plugin): plugin_types.append((modname, t)) except: logging.exception("Error while opening plugin file '%s'" % f) return plugin_types mailnag-1.2.1/Mailnag/common/subproc.py000066400000000000000000000072751272232533200200260ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # subproc.py # # Copyright 2013 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import subprocess import threading import logging import time # Note : All functions of this module *are* thread-safe. # Protects access to the proc dictionary. _proc_lock = threading.Lock() # Intended to prevent simultaneous execution of # (start_subprocess, terminate_subprocesses), # (terminate_subprocesses, terminate_subprocesses). _func_lock = threading.Lock() # Dictionary holding popen objects # and associated thread objects. _procs = {} # Starts a subprocess and an associated thread that waits for # the subprocess to terminate (prevents zombie processes). def start_subprocess(args, shell = False, callback = None): def thread(): t = threading.currentThread() try: p = None with _proc_lock: p = _procs[t] retcode = p.wait() if callback != None: callback(retcode) finally: with _proc_lock: del _procs[t] with _func_lock: p = None pid = -1 try: p = subprocess.Popen(args, shell = shell) except: logging.exception('Caught an exception.') if p != None: pid = p.pid t = threading.Thread(target = thread) with _proc_lock: _procs[t] = p t.start() return pid # Terminates all subprocesses that were started by # start_subprocess(). Subprocesses that don't terminate # within the timeframe (seconds) specified by the # timeout argument, are sent a kill signal. def terminate_subprocesses(timeout = 3.0): with _func_lock: threads = [] with _proc_lock: if len(_procs) == 0: return for t, p in _procs.iteritems(): threads.append(t) # Ask all runnig processes to terminate. # This will also terminate associated threads # waiting for p.wait(). # Note : terminate() does not block. try: p.terminate() except: logging.debug('p.terminate() failed') # Start a watchdog thread that will kill # all processes that didn't terminate within seconds. wd = _Watchdog(timeout) wd.start() # Wait for all threads to terminate for t in threads: t.join() wd.stop() if not wd.triggered: logging.info('All subprocesses exited normally.') # Internal Watchdog class class _Watchdog(threading.Thread): def __init__(self, timeout): threading.Thread.__init__(self) self.triggered = False self._timeout = timeout self._event = threading.Event() def run(self): self._event.wait(self._timeout) if not self._event.is_set(): logging.warning('Process termination took too long - watchdog starts killing...') self.triggered = True with _proc_lock: for t, p in _procs.iteritems(): try: # Kill process p and quit the thread # waiting for p to terminate (p.wait()). p.kill() logging.info('Watchdog killed process %s' % p.pid) except: logging.debug('p.kill() failed') def stop(self): # Abort watchdog thread (may have been triggered already) self._event.set() # Wait for watchdog thread (may be inactive already) self.join() mailnag-1.2.1/Mailnag/common/utils.py000066400000000000000000000053601272232533200175020ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # utils.py # # Copyright 2011 - 2015 Patrick Ulbrich # Copyright 2007 Marco Ferragina # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import xdg.BaseDirectory as base import os import sys import time import dbus import logging import inspect from Mailnag.common.dist_cfg import PACKAGE_NAME, DBUS_BUS_NAME, DBUS_OBJ_PATH def get_data_paths(): # Add "./data" in workdir for running from builddir data_paths = [] data_paths.append("./data") data_paths.extend(base.load_data_paths(PACKAGE_NAME)) return data_paths def get_data_file(filename): """ Return path to @filename if it exists anywhere in the data paths, else return None """ data_paths = get_data_paths() for direc in data_paths: file_path = os.path.join(direc, filename) if os.path.exists(file_path): return file_path return None def splitstr(strn, delimeter): return [s.strip() for s in strn.split(delimeter) if s.strip()] def fix_cwd(): # Change into local Mailnag source dir, where paths # in dist_cfg.py point to (e.g. "./locale"). # Only required when running Mailnag locally (wihout installation). main_script_path = os.path.realpath(inspect.stack()[-1][1]) main_script_dir = os.path.dirname(main_script_path) os.chdir(main_script_dir) def set_procname(newname): from ctypes import cdll, byref, create_string_buffer libc = cdll.LoadLibrary('libc.so.6') buff = create_string_buffer(len(newname)+1) buff.value = newname libc.prctl(15, byref(buff), 0, 0, 0) def try_call(f, err_retval = None): try: return f() except: logging.exception('Caught an exception.') return err_retval def shutdown_existing_instance(): bus = dbus.SessionBus() if bus.name_has_owner(DBUS_BUS_NAME): sys.stdout.write('Shutting down existing Mailnag process...') sys.stdout.flush() try: proxy = bus.get_object(DBUS_BUS_NAME, DBUS_OBJ_PATH) shutdown = proxy.get_dbus_method('Shutdown', DBUS_BUS_NAME) shutdown() while bus.name_has_owner(DBUS_BUS_NAME): time.sleep(2) print 'OK' except: print 'FAILED' mailnag-1.2.1/Mailnag/configuration/000077500000000000000000000000001272232533200173435ustar00rootroot00000000000000mailnag-1.2.1/Mailnag/configuration/__init__.py000066400000000000000000000014711272232533200214570ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # __init__.py # # Copyright 2011 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # mailnag-1.2.1/Mailnag/configuration/accountdialog.py000066400000000000000000000273371272232533200225450ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # accountdialog.py # # Copyright 2011 - 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('Gtk', '3.0') gi.require_version('GLib', '2.0') from gi.repository import GObject, GLib, Gtk from thread import start_new_thread from Mailnag.common.dist_cfg import PACKAGE_NAME from Mailnag.common.i18n import _ from Mailnag.common.utils import get_data_file, splitstr from Mailnag.common.accounts import Account IDX_GMAIL = 0 IDX_GMX = 1 IDX_WEB_DE = 2 IDX_YAHOO = 3 IDX_IMAP = 4 IDX_POP3 = 5 PROVIDER_CONFIGS = [ [ 'Gmail', 'imap.gmail.com', '993'], [ 'GMX', 'imap.gmx.net', '993'], [ 'Web.de', 'imap.web.de', '993'], [ 'Yahoo', 'imap.mail.yahoo.com', '993'] ] class AccountDialog: def __init__(self, parent, acc): self._acc = acc builder = Gtk.Builder() builder.set_translation_domain(PACKAGE_NAME) builder.add_from_file(get_data_file("account_dialog.ui")) builder.connect_signals({ \ "account_type_changed" : self._on_cmb_account_type_changed, \ "entry_changed" : self._on_entry_changed, \ "expander_folders_activate" : self._on_expander_folders_activate, \ "btn_cancel_clicked" : self._on_btn_cancel_clicked, \ "btn_save_clicked" : self._on_btn_save_clicked \ }) self._window = builder.get_object("account_dialog") self._window.set_transient_for(parent) self._cmb_account_type = builder.get_object("cmb_account_type") self._label_account_name = builder.get_object("label_account_name") self._entry_account_name = builder.get_object("entry_account_name") self._entry_account_user = builder.get_object("entry_account_user") self._entry_account_password = builder.get_object("entry_account_password") self._label_account_server = builder.get_object("label_account_server") self._entry_account_server = builder.get_object("entry_account_server") self._label_account_port = builder.get_object("label_account_port") self._entry_account_port = builder.get_object("entry_account_port") self._expander_folders = builder.get_object("expander_folders") self._overlay = builder.get_object("overlay") self._treeview_folders = builder.get_object("treeview_folders") self._liststore_folders = builder.get_object("liststore_folders") self._chk_account_push = builder.get_object("chk_account_push") self._chk_account_ssl = builder.get_object("chk_account_ssl") self._button_save = builder.get_object("button_save") self._error_label = None self._folders_received = False self._selected_folder_count = 0 self._entry_account_port.set_placeholder_text(_("optional")) renderer_folders_enabled = Gtk.CellRendererToggle() renderer_folders_enabled.connect("toggled", self._on_folder_toggled) column_folders_enabled = Gtk.TreeViewColumn(_('Enabled'), renderer_folders_enabled) column_folders_enabled.add_attribute(renderer_folders_enabled, "active", 0) column_folders_enabled.set_alignment(0.5) self._treeview_folders.append_column(column_folders_enabled) renderer_folders_name = Gtk.CellRendererText() column_folders_name = Gtk.TreeViewColumn(_('Name'), renderer_folders_name, text = 1) self._treeview_folders.append_column(column_folders_name) def run(self): self._fill_account_type_cmb() self._load_account(self._acc) res = self._window.run() if res == 1: self._configure_account(self._acc) self._window.destroy() return res def get_account(self): return self._acc def _load_account(self, acc): self._entry_account_name.set_text(acc.name) self._entry_account_user.set_text(acc.user) self._entry_account_password.set_text(acc.password) self._entry_account_server.set_text(acc.server) self._entry_account_port.set_text(acc.port) self._chk_account_push.set_active(acc.idle) self._chk_account_push.set_sensitive(len(acc.folders) < 2) self._chk_account_ssl.set_active(acc.ssl) def _configure_account(self, acc): acctype = self._cmb_account_type.get_active() if (acctype == IDX_POP3) or (acctype == IDX_IMAP): acc.name = self._entry_account_name.get_text() acc.user = self._entry_account_user.get_text() acc.password = self._entry_account_password.get_text() acc.server = self._entry_account_server.get_text() acc.port = self._entry_account_port.get_text() acc.ssl = self._chk_account_ssl.get_active() if acctype == IDX_POP3: acc.imap = False acc.folders = [] acc.idle = False elif acctype == IDX_IMAP: acc.imap = True if self._folders_received: acc.folders = self._get_selected_folders() acc.idle = self._chk_account_push.get_active() else: # known provider (imap only) acc.name = self._entry_account_user.get_text() acc.user = self._entry_account_user.get_text() acc.password = self._entry_account_password.get_text() acc.ssl = True acc.imap = True if self._folders_received: acc.folders = self._get_selected_folders() acc.idle = (len(acc.folders) < 2) if acctype < len(PROVIDER_CONFIGS): p = PROVIDER_CONFIGS[acctype] acc.name += (' (%s)' % p[0]) acc.server = p[1] acc.port = p[2] else: raise Exception('Unknown account type') def _get_selected_folders(self): folders = [] for row in self._liststore_folders: if row[0]: folders.append(row[1]) return folders def _fill_account_type_cmb(self): # fill acount type cmb for p in PROVIDER_CONFIGS: self._cmb_account_type.append_text(p[0]) self._cmb_account_type.append_text(_("Other (IMAP)")) self._cmb_account_type.append_text(_("Other (POP3)")) # select account type if len(self._acc.server) == 0: # default to Gmail when creating new accounts self._cmb_account_type.set_active(IDX_GMAIL) # triggers _on_cmb_account_type_changed() else: i = 0 idx = -1 for p in PROVIDER_CONFIGS: if (('%s (%s)' % (self._acc.user, p[0])) == self._acc.name) and \ p[1] == self._acc.server and \ p[2] == self._acc.port: idx = i break i+=1 if idx >= 0: self._cmb_account_type.set_active(idx) # triggers _on_cmb_account_type_changed() else: self._cmb_account_type.set_active(IDX_IMAP if self._acc.imap else IDX_POP3) # triggers _on_cmb_account_type_changed() # Don't allow changing the account type if the loaded account has folders. self._cmb_account_type.set_sensitive(len(self._acc.folders) == 0) def _on_btn_cancel_clicked(self, widget): pass def _on_btn_save_clicked(self, widget): pass def _on_entry_changed(self, widget): # # Validate # acctype = self._cmb_account_type.get_active() if (acctype == IDX_POP3) or (acctype == IDX_IMAP): ok = len(self._entry_account_name.get_text()) > 0 and \ len(self._entry_account_user.get_text()) > 0 and \ len(self._entry_account_password.get_text()) > 0 and \ len(self._entry_account_server.get_text()) > 0 else: # known provider ok = len(self._entry_account_user.get_text()) > 0 and \ len(self._entry_account_password.get_text()) > 0 self._expander_folders.set_sensitive(self._folders_received or ok) self._button_save.set_sensitive(ok) def _on_expander_folders_activate(self, widget): # Folder list has already been loaded or # expander is going to be closed -> do nothing. if self._folders_received or \ self._expander_folders.get_expanded(): return if self._error_label != None: self._error_label.destroy() self._error_label = None spinner = Gtk.Spinner() spinner.set_halign(Gtk.Align.CENTER) spinner.set_valign(Gtk.Align.CENTER) spinner.start() self._overlay.add_overlay(spinner) self._overlay.show_all() # Executed on a new worker thread def worker_thread(name): folders = [] exception = None try: acc = Account() self._configure_account(acc) folders = acc.request_server_folders() except Exception as ex: exception = ex # Executed on the GTK main thread def finished_func(): spinner.stop() spinner.destroy() if exception != None: self._error_label = Gtk.Label() self._error_label.set_justify(Gtk.Justification.CENTER) self._error_label.set_halign(Gtk.Align.CENTER) self._error_label.set_valign(Gtk.Align.CENTER) self._error_label.set_markup('%s' % _('Connection failed.')) self._overlay.add_overlay(self._error_label) self._overlay.show_all() else: for f in folders: enabled = False if f in self._acc.folders: enabled = True self._selected_folder_count += 1 row = [enabled, f] self._liststore_folders.append(row) # Enable the push checkbox in case a remote folder wasn't found # and the folder count is now <2. # (e.g. folders have been renamed/removed on the server, the user has entered a # diffent username/password in this dialog, ...) self._chk_account_push.set_sensitive(self._selected_folder_count < 2) self._folders_received = True GObject.idle_add(finished_func) start_new_thread(worker_thread, ("worker_thread",)) def _on_folder_toggled(self, cell, path): isactive = not cell.get_active() model = self._liststore_folders iter = model.get_iter(path) self._liststore_folders.set_value(iter, 0, isactive) if isactive: self._selected_folder_count += 1 else: self._selected_folder_count -= 1 if self._selected_folder_count < 2: self._chk_account_push.set_sensitive(True) else: self._chk_account_push.set_active(False) self._chk_account_push.set_sensitive(False) def _on_cmb_account_type_changed(self, widget): acctype = widget.get_active() # # Reset everything when the account type changes # if acctype == IDX_POP3: self._label_account_name.set_visible(True) self._entry_account_name.set_visible(True) self._label_account_server.set_visible(True) self._entry_account_server.set_visible(True) self._label_account_port.set_visible(True) self._entry_account_port.set_visible(True) self._expander_folders.set_visible(False) self._chk_account_push.set_visible(False) self._chk_account_ssl.set_visible(True) elif acctype == IDX_IMAP: self._label_account_name.set_visible(True) self._entry_account_name.set_visible(True) self._label_account_server.set_visible(True) self._entry_account_server.set_visible(True) self._label_account_port.set_visible(True) self._entry_account_port.set_visible(True) self._expander_folders.set_visible(True) self._chk_account_push.set_visible(True) self._chk_account_ssl.set_visible(True) else: # known provider (imap only) self._label_account_name.set_visible(False) self._entry_account_name.set_visible(False) self._label_account_server.set_visible(False) self._entry_account_server.set_visible(False) self._label_account_port.set_visible(False) self._entry_account_port.set_visible(False) self._expander_folders.set_visible(True) self._chk_account_push.set_visible(False) self._chk_account_ssl.set_visible(False) self._folders_received = False self._selected_folder_count = 0 self._expander_folders.set_expanded(False) self._liststore_folders.clear() empty_acc = Account() self._load_account(empty_acc) mailnag-1.2.1/Mailnag/configuration/configwindow.py000066400000000000000000000326151272232533200224210ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # configwindow.py # # Copyright 2011 - 2016 Patrick Ulbrich # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('Gtk', '3.0') gi.require_version('GLib', '2.0') import os import shutil import xdg.BaseDirectory as bd from gi.repository import GLib, GdkPixbuf, Gdk, Gtk, GObject from Mailnag.common.dist_cfg import PACKAGE_NAME, APP_VERSION, BIN_DIR, DESKTOP_FILE_DIR from Mailnag.common.i18n import _ from Mailnag.common.utils import get_data_file, get_data_paths from Mailnag.common.config import read_cfg, write_cfg from Mailnag.common.accounts import Account, AccountManager from Mailnag.common.credentialstore import CredentialStore from Mailnag.common.plugins import Plugin from Mailnag.configuration.accountdialog import AccountDialog from Mailnag.configuration.plugindialog import PluginDialog class ConfigWindow: def __init__(self): builder = Gtk.Builder() builder.set_translation_domain(PACKAGE_NAME) builder.add_from_file(get_data_file("config_window.ui")) builder.connect_signals({ \ "config_window_deleted" : self._on_config_window_deleted, \ "btn_page_toggled" : self._on_btn_page_toggled, \ "btn_add_account_clicked" : self._on_btn_add_account_clicked, \ "btn_edit_account_clicked" : self._on_btn_edit_account_clicked, \ "btn_remove_account_clicked" : self._on_btn_remove_account_clicked, \ "treeview_accounts_row_activated" : self._on_treeview_accounts_row_activated, \ "liststore_accounts_row_deleted" : self._on_liststore_accounts_row_deleted, \ "liststore_accounts_row_inserted" : self._on_liststore_accounts_row_inserted, \ "btn_edit_plugin_clicked" : self._on_btn_edit_plugin_clicked, \ "treeview_plugins_row_activated" : self._on_treeview_plugins_row_activated, \ "treeview_plugins_cursor_changed" : self._on_treeview_plugins_cursor_changed, \ }) # Add icons in alternative data paths (e.g. ./data/icons) # to the icon search path in case Mailnag is launched # from a local directory (without installing). icon_theme = Gtk.IconTheme.get_default() for path in get_data_paths(): icon_theme.append_search_path(os.path.join(path, "icons")) self._window = builder.get_object("config_window") self._window.set_icon_name("mailnag") self._load_stylesheet('config_window.css') self._cfg = read_cfg() self.daemon_enabled = False # # toggle buttons / notebook # self._notebook = builder.get_object("notebook") self._box_navigation = builder.get_object("box_navigation") self._box_navigation.get_children()[0].set_active(True) # # general page # # The dimension of the png is expected to be 180x180 px pb = GdkPixbuf.Pixbuf.new_from_file(get_data_file("mailnag.png")) pb = pb.new_subpixbuf(0, 10, 180, 146) # crop whitespace at the bottom self._image_logo = builder.get_object("image_logo") self._image_logo.set_from_pixbuf(pb) self._label_app_desc = builder.get_object("label_app_desc") self._label_app_desc.set_markup("Mailnag\nVersion %s" % str(APP_VERSION)) self._switch_daemon_enabled = builder.get_object("switch_daemon_enabled") # # accounts page # self._accountman = AccountManager(CredentialStore.from_string(self._cfg.get('core', 'credentialstore'))) self._treeview_accounts = builder.get_object("treeview_accounts") self._liststore_accounts = builder.get_object("liststore_accounts") self._button_edit_account = builder.get_object("btn_edit_account") self._button_remove_account = builder.get_object("btn_remove_account") self._button_edit_account.set_sensitive(False) self._button_remove_account.set_sensitive(False) renderer_acc_enabled = Gtk.CellRendererToggle() renderer_acc_enabled.connect("toggled", self._on_account_toggled) column_acc_enabled = Gtk.TreeViewColumn(_('Enabled'), renderer_acc_enabled) column_acc_enabled.add_attribute(renderer_acc_enabled, "active", 1) column_acc_enabled.set_alignment(0.5) self._treeview_accounts.append_column(column_acc_enabled) renderer_acc_name = Gtk.CellRendererText() column_acc_name = Gtk.TreeViewColumn(_('Name'), renderer_acc_name, text = 2) self._treeview_accounts.append_column(column_acc_name) # # plugins page # self._treeview_plugins = builder.get_object("treeview_plugins") self._liststore_plugins = builder.get_object("liststore_plugins") self._button_edit_plugin = builder.get_object("btn_edit_plugin") self._button_edit_plugin.set_sensitive(False) def renderer_plugin_enabled_func(column, cell_renderer, model, iter, data): plugin = model.get_value(iter, 0) name, desc, ver, author, mandatory = plugin.get_manifest() cell_renderer.set_sensitive(not mandatory) renderer_plugin_enabled = Gtk.CellRendererToggle() renderer_plugin_enabled.connect("toggled", self._on_plugin_toggled) column_plugin_enabled = Gtk.TreeViewColumn(_('Enabled'), renderer_plugin_enabled) column_plugin_enabled.add_attribute(renderer_plugin_enabled, "active", 1) column_plugin_enabled.set_alignment(0.5) column_plugin_enabled.set_cell_data_func(renderer_plugin_enabled, renderer_plugin_enabled_func) self._treeview_plugins.append_column(column_plugin_enabled) renderer_plugin_name = Gtk.CellRendererText() column_plugin_name = Gtk.TreeViewColumn(_('Name'), renderer_plugin_name, markup = 2) self._treeview_plugins.append_column(column_plugin_name) # load config self._load_config() self._window.show() def _load_stylesheet(self, stylesheet): provider = Gtk.CssProvider() provider.load_from_path(get_data_file(stylesheet)) Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def _load_config(self): self._switch_daemon_enabled.set_active(bool(int(self._cfg.get('core', 'autostart')))) self._accountman.load_from_cfg(self._cfg) # load accounts for acc in self._accountman: row = [acc, acc.enabled, acc.name] self._liststore_accounts.append(row) self._select_account_path((0,)) # load plugins enabled_lst = self._cfg.get('core', 'enabled_plugins').split(',') enabled_lst = filter(lambda s: s != '', map(lambda s: s.strip(), enabled_lst)) plugins = Plugin.load_plugins(self._cfg) plugins.sort(key = lambda p : (not p.get_manifest()[4], p.get_manifest()[0])) for plugin in plugins: name, desc, ver, author, mandatory = plugin.get_manifest() enabled = True if (plugin.get_modname() in enabled_lst) or mandatory else False description = '%s (%s)\n%s' % (name, ver, desc) row = [plugin, enabled, description] self._liststore_plugins.append(row) self._select_plugin_path((0,)) def _save_config(self): autostart = self._switch_daemon_enabled.get_active() self._cfg.set('core', 'autostart', int(autostart)) self._accountman.save_to_cfg(self._cfg) enabled_plugins = '' for row in self._liststore_plugins: plugin = row[0] modname = plugin.get_modname() if row[1] == True: if len(enabled_plugins) > 0: enabled_plugins += ', ' enabled_plugins += modname config = plugin.get_config() if len(config) > 0: if not self._cfg.has_section(modname): self._cfg.add_section(modname) for k, v in config.iteritems(): self._cfg.set(modname, k, v) self._cfg.set('core', 'enabled_plugins', enabled_plugins) write_cfg(self._cfg) if autostart: self._create_autostart() else: self._delete_autostart() def _show_confirmation_dialog(self, text): message = Gtk.MessageDialog(self._window, Gtk.DialogFlags.MODAL, \ Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, text) resp = message.run() message.destroy() if resp == Gtk.ResponseType.YES: return True else: return False def _get_selected_account(self): treeselection = self._treeview_accounts.get_selection() selection = treeselection.get_selected() model, iter = selection # get account object from treeviews 1st column if iter != None: acc = model.get_value(iter, 0) else: acc = None return acc, model, iter def _select_account_path(self, path): treeselection = self._treeview_accounts.get_selection() treeselection.select_path(path) self._treeview_accounts.grab_focus() def _edit_account(self): acc, model, iter = self._get_selected_account() if iter != None: d = AccountDialog(self._window, acc) if d.run() == 1: model.set_value(iter, 2, acc.name) def _get_selected_plugin(self): treeselection = self._treeview_plugins.get_selection() selection = treeselection.get_selected() model, iter = selection # get plugin object from treeviews 1st column if iter != None: plugin = model.get_value(iter, 0) else: plugin = None return plugin, model, iter def _select_plugin_path(self, path): treeselection = self._treeview_plugins.get_selection() treeselection.select_path(path) self._treeview_plugins.grab_focus() def _edit_plugin(self): plugin, model, iter = self._get_selected_plugin() if (iter != None) and plugin.has_config_ui(): d = PluginDialog(self._window, plugin) d.run() def _create_autostart(self): autostart_folder = os.path.join(bd.xdg_config_home, "autostart") src = os.path.join(DESKTOP_FILE_DIR, "mailnag.desktop") dst = os.path.join(autostart_folder, "mailnag.desktop") if not os.path.exists(autostart_folder): os.makedirs(autostart_folder) shutil.copyfile(src, dst) # If mailag-config was started from a local directory, # patch the exec path of the autostart .desktop file accordingly. if not os.path.isabs(DESKTOP_FILE_DIR): exec_file = os.path.join(os.path.abspath(BIN_DIR), "mailnag") with open(dst, 'r') as f: strn = f.read() strn = strn.replace('/usr/bin/mailnag', exec_file) with open(dst, 'w') as f: f.write(strn) def _delete_autostart(self): autostart_folder = os.path.join(bd.xdg_config_home, "autostart") autostart_file = autostart_folder + "mailnag.desktop" if os.path.exists(autostart_file): os.remove(autostart_file) def _on_btn_page_toggled(self, button): if not button.get_active(): return page = 0 for btn in self._box_navigation.get_children(): if btn == button: self._notebook.set_current_page(page) else: btn.set_active(False) page += 1 def _on_account_toggled(self, cell, path): model = self._liststore_accounts iter = model.get_iter(path) acc = model.get_value(iter, 0) acc.enabled = not acc.enabled self._liststore_accounts.set_value(iter, 1, not cell.get_active()) def _on_btn_add_account_clicked(self, widget): acc = Account(enabled = True) d = AccountDialog(self._window, acc) if d.run() == 1: self._accountman.add(acc) row = [acc, acc.enabled, acc.name] iter = self._liststore_accounts.append(row) model = self._treeview_accounts.get_model() path = model.get_path(iter) self._treeview_accounts.set_cursor(path, None, False) self._treeview_accounts.grab_focus() def _on_btn_edit_account_clicked(self, widget): self._edit_account() def _on_btn_remove_account_clicked(self, widget): acc, model, iter = self._get_selected_account() if iter != None: if self._show_confirmation_dialog(_('Delete this account:') + \ '\n\n' + acc.name): # select prev/next account p = model.get_path(iter) if not p.prev(): p.next() self._select_account_path(p) # remove from treeview model.remove(iter) # remove from account manager self._accountman.remove(acc) def _on_treeview_accounts_row_activated(self, treeview, path, view_column): self._edit_account() def _on_liststore_accounts_row_deleted(self, model, path): self._button_edit_account.set_sensitive(len(model) > 0) self._button_remove_account.set_sensitive(len(model) > 0) def _on_liststore_accounts_row_inserted(self, model, path, user_param): self._button_edit_account.set_sensitive(len(model) > 0) self._button_remove_account.set_sensitive(len(model) > 0) def _on_plugin_toggled(self, cell, path): model = self._liststore_plugins iter = model.get_iter(path) self._liststore_plugins.set_value(iter, 1, not cell.get_active()) def _on_btn_edit_plugin_clicked(self, widget): self._edit_plugin() def _on_treeview_plugins_row_activated(self, treeview, path, view_column): self._edit_plugin() def _on_treeview_plugins_cursor_changed(self, treeview): # Workaround for a bug in GTK < 3.8, # see http://permalink.gmane.org/gmane.comp.gnome.svn/694089 if not self._window.get_visible(): return plugin, model, iter = self._get_selected_plugin() if iter != None: self._button_edit_plugin.set_sensitive(plugin.has_config_ui()) def _save_and_quit(self): self._save_config() self.daemon_enabled = self._switch_daemon_enabled.get_active() Gtk.main_quit() def _on_config_window_deleted(self, widget, event): self._save_and_quit() mailnag-1.2.1/Mailnag/configuration/plugindialog.py000066400000000000000000000032531272232533200223760ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # plugindialog.py # # Copyright 2013 - 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from Mailnag.common.i18n import _ class PluginDialog: def __init__(self, parent, plugin): self._plugin = plugin flags = Gtk.DialogFlags.MODAL # | Gtk.DialogFlags.USE_HEADER_BAR self._window = Gtk.Dialog(_('Plugin Configuration'), parent, flags, \ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)) self._box = self._window.get_content_area() self._box.set_border_width(6) self._box.set_spacing(6) def run(self): widget = self._plugin.get_config_ui() if widget != None: self._box.add(widget) widget.show_all() self._plugin.load_ui_from_config(widget) res = self._window.run() if res == Gtk.ResponseType.OK: if widget != None: self._plugin.save_ui_to_config(widget) self._window.destroy() return res mailnag-1.2.1/Mailnag/daemon/000077500000000000000000000000001272232533200157375ustar00rootroot00000000000000mailnag-1.2.1/Mailnag/daemon/__init__.py000066400000000000000000000014711272232533200200530ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # __init__.py # # Copyright 2011 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # mailnag-1.2.1/Mailnag/daemon/conntest.py000066400000000000000000000044561272232533200201570ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # conntest.py # # Copyright 2014 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import os import dbus PING_TEST_HOST = 'www.google.de' NM_STATE_CONNECTED_GLOBAL = 70 NM_PATH = '/org/freedesktop/NetworkManager' NM_NAME = 'org.freedesktop.NetworkManager' DBUS_PROPS_NAME = 'org.freedesktop.DBus.Properties' # TODO: Add GLib connection mode if GLib > 2.42 is available # (wrap GNetworkmonitor.get_connectivity() into a try/except block) # TODO: Make this class an enum # when Mailnag is ported to python3 class TestModes: AUTO = 0 NETWORKMANAGER = 1 PING = 2 class ConnectivityTest: def __init__(self, testmode): self._testmode = testmode self._nm_is_offline = True bus = dbus.SystemBus() if self._testmode == TestModes.AUTO: if bus.name_has_owner(NM_NAME): self._testmode = TestModes.NETWORKMANAGER else: self._testmode = TestModes.PING if self._testmode == TestModes.NETWORKMANAGER: def state_changed_handler(state): self._nm_is_offline = (state != NM_STATE_CONNECTED_GLOBAL) proxy = bus.get_object(NM_NAME, NM_PATH) # Note: connect requires DBusGMainLoop(set_as_default = True) # and a running main loop. proxy.connect_to_signal('StateChanged', state_changed_handler) iface = dbus.Interface(proxy, DBUS_PROPS_NAME) state = iface.Get(NM_NAME, 'State') self._nm_is_offline = (state != NM_STATE_CONNECTED_GLOBAL) def is_offline(self): if self._testmode == TestModes.NETWORKMANAGER: return self._nm_is_offline else: return (os.system('ping -c1 -W2 %s > /dev/null 2>&1' % PING_TEST_HOST) != 0) mailnag-1.2.1/Mailnag/daemon/idlers.py000066400000000000000000000124201272232533200175720ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # idlers.py # # Copyright 2011 - 2016 Patrick Ulbrich # Copyright 2011 Leighton Earl # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import threading import time import logging from Mailnag.common.imaplib2 import AUTH from Mailnag.common.exceptions import InvalidOperationException # # Idler class # class Idler(object): def __init__(self, account, sync_callback, idle_timeout): self.RECONNECT_RETRY_INTERVAL = 5 # minutes self._thread = threading.Thread(target=self._idle) self._event = threading.Event() self._sync_callback = sync_callback self._account = account self._idle_timeout = idle_timeout # use_existing = True: # connection has been opened in mailnagdaemon.py already (immediate check) self._conn = account.get_connection(use_existing = True) self._disposed = False # Need to get out of AUTH mode of fresh connections. if self._conn.state == AUTH: self._select(self._conn, account) def start(self): if self._disposed: raise InvalidOperationException("Idler has been disposed") self._thread.start() def dispose(self): if self._thread.is_alive(): self._event.set() self._thread.join() try: if self._conn != None: # Exit possible active idle state. # (also calls idle_callback) self._conn.noop() except: pass self._disposed = True logging.info('Idler closed') # idle thread def _idle(self): while True: # if the event is set here, # disposed() must have been called # so stop the idle thread. if self._event.isSet(): return self._needsync = False self._conn_closed = False # register idle callback that is called whenever an idle event arrives (new mail / mail deleted). # the callback is called after minutes at the latest. # gmail sends keepalive events every 5 minutes. self._conn.idle(callback = self._idle_callback, timeout = 60 * self._idle_timeout) # waits for the event to be set # (in idle callback or in dispose()) self._event.wait() # if the event is set due to idle sync if self._needsync: self._event.clear() if self._conn_closed: self._reconnect() if self._conn != None: self._sync_callback(self._account) # idle callback (runs on a further thread) def _idle_callback(self, args): # check if the connection has been reset by provider self._conn_closed = (args[2] != None) and (args[2][0] is self._conn.abort) # flag that a mail sync is needed self._needsync = True # trigger waiting _idle thread self._event.set() def _reconnect(self): # connection has been reset by provider -> try to reconnect logging.info("Idler thread for account '%s' has been disconnected" % self._account.name) # conn has already been closed, don't try to close it again # self._conn.close() # (calls idle_callback) # shutdown existing callback thread self._conn.logout() self._conn = None while (self._conn == None) and (not self._event.isSet()): logging.info("Trying to reconnect Idler thread for account '%s'." % self._account.name) try: self._conn = self._account.get_connection(use_existing = False) logging.info("Successfully reconnected Idler thread for account '%s'." % self._account.name) except Exception as ex: logging.error("Failed to reconnect Idler thread for account '%s' (%s)." % (self._account.name, ex)) logging.info("Trying to reconnect Idler thread for account '%s' in %s minutes" % (self._account.name, str(self.RECONNECT_RETRY_INTERVAL))) self._wait(60 * self.RECONNECT_RETRY_INTERVAL) # don't hammer the server if self._conn != None: self._select(self._conn, self._account) def _select(self, conn, account): if len(account.folders) == 1: conn.select(account.folders[0]) else: conn.select("INBOX") def _wait(self, secs): start_time = time.time() while (((time.time() - start_time) < secs) and (not self._event.isSet())): time.sleep(1) # # IdlerRunner class # class IdlerRunner: def __init__(self, accounts, sync_callback, idle_timeout): self._idlerlist = [] self._accounts = accounts self._sync_callback = sync_callback self._idle_timeout = idle_timeout def start(self): for acc in self._accounts: if acc.imap and acc.idle: try: idler = Idler(acc, self._sync_callback, self._idle_timeout) idler.start() self._idlerlist.append(idler) except Exception as ex: logging.error("Error: Failed to create an idler thread for account '%s' (%s)" % (acc.name, ex)) def dispose(self): for idler in self._idlerlist: idler.dispose() mailnag-1.2.1/Mailnag/daemon/mailchecker.py000066400000000000000000000063141272232533200205640ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # mailchecker.py # # Copyright 2011 - 2016 Patrick Ulbrich # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import threading import logging from Mailnag.common.utils import try_call from Mailnag.common.i18n import _ from Mailnag.common.plugins import HookTypes from Mailnag.daemon.mails import MailSyncer class MailChecker: def __init__(self, cfg, memorizer, hookreg, conntest): self._firstcheck = True # first check after startup self._mailcheck_lock = threading.Lock() self._mailsyncer = MailSyncer(cfg) self._memorizer = memorizer self._hookreg = hookreg self._conntest = conntest self._zero_mails_on_last_check = True def check(self, accounts): # make sure multiple threads (idler and polling thread) # don't check for mails simultaneously. with self._mailcheck_lock: logging.info('Checking %s email account(s).' % len(accounts)) for f in self._hookreg.get_hook_funcs(HookTypes.MAIL_CHECK): try_call( f ) if self._conntest.is_offline(): logging.warning('No internet connection.') return all_mails = self._mailsyncer.sync(accounts) unseen_mails = [] new_mails = [] for mail in all_mails: if self._memorizer.contains(mail.id): # mail was fetched before if self._memorizer.is_unseen(mail.id): # mail was not marked as seen unseen_mails.append(mail) if self._firstcheck: new_mails.append(mail) else: # mail is fetched the first time unseen_mails.append(mail) new_mails.append(mail) self._memorizer.sync(all_mails) self._memorizer.save() self._firstcheck = False # apply filter plugin hooks filtered_unseen_mails = unseen_mails for f in self._hookreg.get_hook_funcs(HookTypes.FILTER_MAILS): filtered_unseen_mails = try_call( lambda: f(filtered_unseen_mails), filtered_unseen_mails ) filtered_new_mails = [m for m in new_mails if m in filtered_unseen_mails] if len(filtered_new_mails) > 0: for f in self._hookreg.get_hook_funcs(HookTypes.MAILS_ADDED): try_call( lambda: f(filtered_new_mails, filtered_unseen_mails) ) elif (not self._zero_mails_on_last_check) and (len(filtered_unseen_mails) == 0): # TODO : signal MailsRemoved if not all mails have been removed # (i.e. if mailcount has been decreased) for f in self._hookreg.get_hook_funcs(HookTypes.MAILS_REMOVED): try_call( lambda: f(filtered_unseen_mails) ) self._zero_mails_on_last_check = (len(filtered_unseen_mails) == 0) return mailnag-1.2.1/Mailnag/daemon/mailnagdaemon.py000066400000000000000000000214711272232533200211120ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # mailnagdaemon.py # # Copyright 2014, 2015 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import threading import logging import time from Mailnag.common.accounts import AccountManager from Mailnag.common.credentialstore import CredentialStore from Mailnag.daemon.mailchecker import MailChecker from Mailnag.daemon.mails import Memorizer from Mailnag.daemon.idlers import IdlerRunner from Mailnag.daemon.conntest import ConnectivityTest, TestModes from Mailnag.common.plugins import Plugin, HookRegistry, HookTypes, MailnagController from Mailnag.common.exceptions import InvalidOperationException from Mailnag.common.config import read_cfg from Mailnag.common.utils import try_call testmode_mapping = { 'auto' : TestModes.AUTO, 'networkmanager' : TestModes.NETWORKMANAGER, 'ping' : TestModes.PING } class MailnagDaemon: def __init__(self, fatal_error_handler = None, shutdown_request_handler = None): self._cfg = None self._fatal_error_handler = fatal_error_handler self._shutdown_request_handler = shutdown_request_handler self._plugins = [] self._hookreg = None self._conntest = None self._accounts = None self._mailchecker = None self._start_thread = None self._poll_thread = None self._poll_thread_stop = threading.Event() self._idlrunner = None # Lock ensures that init() and dispose() # are non-reentrant. self._lock = threading.Lock() # Flag indicating complete # daemon initialization. self._initialized = False self._disposed = False # Initializes the daemon and starts checking threads. def init(self): with self._lock: if self._disposed: raise InvalidOperationException("Daemon has been disposed") if self._initialized: raise InvalidOperationException("Daemon has already been initialized") self._cfg = read_cfg() accountman = AccountManager(CredentialStore.from_string(self._cfg.get('core', 'credentialstore'))) accountman.load_from_cfg(self._cfg, enabled_only = True) self._accounts = accountman.to_list() self._hookreg = HookRegistry() self._conntest = ConnectivityTest(testmode_mapping[self._cfg.get('core', 'connectivity_test')]) memorizer = Memorizer() memorizer.load() self._mailchecker = MailChecker(self._cfg, memorizer, self._hookreg, self._conntest) # Note: all code following _load_plugins() should be executed # asynchronously because the dbus plugin requires an active mainloop # (usually started in the programs main function). self._load_plugins(self._cfg, self._hookreg, memorizer) # Start checking for mails asynchronously. self._start_thread = threading.Thread(target = self._start) self._start_thread.start() self._initialized = True def dispose(self): with self._lock: if self._disposed: return # Note: _disposed must be set # before cleaning up resources # (in case an exception occurs) # and before unloading plugins. # Also required by _wait_for_inet_connection(). self._disposed = True # clean up resources if (self._start_thread != None) and (self._start_thread.is_alive()): self._start_thread.join() logging.info('Starter thread exited successfully.') if (self._poll_thread != None) and (self._poll_thread.is_alive()): self._poll_thread_stop.set() self._poll_thread.join() logging.info('Polling thread exited successfully.') if self._idlrunner != None: self._idlrunner.dispose() if self._accounts != None: for acc in self._accounts: if acc.has_connection(): conn = acc.get_connection(use_existing = True) if acc.imap: conn.close() conn.logout() else: conn.quit() self._unload_plugins() def is_initialized(self): return self._initialized def is_disposed(self): return self._disposed # Enforces manual mail checks def check_for_mails(self): # Don't allow mail checks before initialization or # after object disposal. F.i. plugins may not be # loaded/unloaded completely or connections may # have been closed already. self._ensure_valid_state() non_idle_accounts = filter(lambda acc: (not acc.imap) or (acc.imap and not acc.idle), self._accounts) self._mailchecker.check(non_idle_accounts) def _ensure_valid_state(self): if not self._initialized: raise InvalidOperationException( "Daemon has not been initialized") if self._disposed: raise InvalidOperationException( "Daemon has been disposed") def _start(self): try: # Call Accounts-Loaded plugin hooks for f in self._hookreg.get_hook_funcs(HookTypes.ACCOUNTS_LOADED): try_call( lambda: f(self._accounts) ) if not self._wait_for_inet_connection(): return # Immediate check, check *all* accounts try: self._mailchecker.check(self._accounts) except: logging.exception('Caught an exception.') idle_accounts = filter(lambda acc: acc.imap and acc.idle, self._accounts) non_idle_accounts = filter(lambda acc: (not acc.imap) or (acc.imap and not acc.idle), self._accounts) # start polling thread for POP3 accounts and # IMAP accounts without idle support if len(non_idle_accounts) > 0: poll_interval = int(self._cfg.get('core', 'poll_interval')) def poll_func(): try: while True: self._poll_thread_stop.wait(timeout = 60.0 * poll_interval) if self._poll_thread_stop.is_set(): break self._mailchecker.check(non_idle_accounts) except: logging.exception('Caught an exception.') self._poll_thread = threading.Thread(target = poll_func) self._poll_thread.start() # start idler threads for IMAP accounts with idle support if len(idle_accounts) > 0: def sync_func(account): try: self._mailchecker.check([account]) except: logging.exception('Caught an exception.') idle_timeout = int(self._cfg.get('core', 'imap_idle_timeout')) self._idlrunner = IdlerRunner(idle_accounts, sync_func, idle_timeout) self._idlrunner.start() except Exception as ex: logging.exception('Caught an exception.') if self._fatal_error_handler != None: self._fatal_error_handler(ex) def _wait_for_inet_connection(self): if self._conntest.is_offline(): logging.info('Waiting for internet connection...') while True: if self._disposed: return False if not self._conntest.is_offline(): return True # Note: don't sleep too long # (see timeout in mailnag.cleanup()) # ..but also don't sleep to short in case of a ping connection test. time.sleep(3) def _load_plugins(self, cfg, hookreg, memorizer): class MailnagController_Impl(MailnagController): def __init__(self, daemon, memorizer, hookreg, shutdown_request_hdlr): self._daemon = daemon self._memorizer = memorizer self._hookreg = hookreg self._shutdown_request_handler = shutdown_request_hdlr def get_hooks(self): return self._hookreg def shutdown(self): if self._shutdown_request_handler != None: self._shutdown_request_handler() def check_for_mails(self): self._daemon.check_for_mails() def mark_mail_as_read(self, mail_id): # Note: ensure_valid_state() is not really necessary here # (the memorizer object is available in init() and dispose()), # but better be consistent with other daemon methods. self._daemon._ensure_valid_state() self._memorizer.set_to_seen(mail_id) self._memorizer.save() controller = MailnagController_Impl(self, memorizer, hookreg, self._shutdown_request_handler) enabled_lst = cfg.get('core', 'enabled_plugins').split(',') enabled_lst = filter(lambda s: s != '', map(lambda s: s.strip(), enabled_lst)) self._plugins = Plugin.load_plugins(cfg, controller, enabled_lst) for p in self._plugins: try: p.enable() logging.info("Successfully enabled plugin '%s'." % p.get_modname()) except: logging.error("Failed to enable plugin '%s'." % p.get_modname()) def _unload_plugins(self): if len(self._plugins) > 0: err = False for p in self._plugins: try: p.disable() except: err = True logging.error("Failed to disable plugin '%s'." % p.get_modname()) if not err: logging.info('Plugins disabled successfully.') mailnag-1.2.1/Mailnag/daemon/mails.py000066400000000000000000000257341272232533200174310ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # mails.py # # Copyright 2011 - 2016 Patrick Ulbrich # Copyright 2011 Leighton Earl # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import time import email import os import logging import hashlib from email.header import decode_header from Mailnag.common.i18n import _ from Mailnag.common.config import cfg_folder # # Mail class # class Mail: def __init__(self, datetime, subject, sender, id, account): self.datetime = datetime self.subject = subject self.sender = sender self.account_name = account.name self.account_id = account.get_id() self.id = id # # MailCollector class # class MailCollector: def __init__(self, cfg, accounts): self._cfg = cfg self._accounts = accounts def collect_mail(self, sort = True): mail_list = [] mail_ids = {} for acc in self._accounts: # get server connection for this account conn = None try: conn = acc.get_connection(use_existing = True) except Exception as ex: logging.error("Failed to connect to account '%s' (%s)." % (acc.name, ex)) continue if acc.imap: # IMAP if len(acc.folders) == 0: folder_list = [ 'INBOX' ] else: folder_list = acc.folders for folder in folder_list: # select IMAP folder conn.select(folder, readonly = True) try: status, data = conn.search(None, 'UNSEEN') # ALL or UNSEEN except: logging.warning('Folder %s does not exist.', folder) continue if status != 'OK' or None in [d for d in data]: logging.debug('Folder %s in status %s | Data: %s', (folder, status, data)) continue # Bugfix LP-735071 for num in data[0].split(): typ, msg_data = conn.fetch(num, '(BODY.PEEK[HEADER])') # header only (without setting READ flag) for response_part in msg_data: if isinstance(response_part, tuple): try: msg = email.message_from_string(response_part[1]) except: logging.debug("Couldn't get IMAP message.") continue sender, subject, datetime, msgid = self._get_header(msg) id = self._get_id(msgid, acc, folder, sender, subject, datetime) # Discard mails with identical IDs (caused # by mails with a non-unique fallback ID, # i.e. mails received in the same folder with # identical sender and subject but *no datetime*, # see _get_id()). # Also filter duplicates caused by Gmail labels. if id not in mail_ids: mail_list.append(Mail(datetime, subject, \ sender, id, acc)) mail_ids[id] = None # don't close IMAP idle connections if not acc.idle: conn.close() conn.logout() else: # POP # number of mails on the server mail_total = len(conn.list()[1]) for i in range(1, mail_total + 1): # for each mail try: # header plus first 0 lines from body message = conn.top(i, 0)[1] except: logging.debug("Couldn't get POP message.") continue # convert list to string message_string = '\n'.join(message) try: # put message into email object and make a dictionary msg = dict(email.message_from_string(message_string)) except: logging.debug("Couldn't get msg from POP message.") continue sender, subject, datetime, msgid = self._get_header(msg) id = self._get_id(msgid, acc, '', sender, subject, datetime) # Discard mails with identical IDs (caused # by mails with a non-unique fallback ID, # i.e. mails with identical sender and subject # but *no datetime*, see _get_id()). if id not in mail_ids: mail_list.append(Mail(datetime, subject, sender, \ id, acc)) mail_ids[id] = None # disconnect from Email-Server conn.quit() # sort mails if sort: mail_list = sort_mails(mail_list, sort_desc = True) return mail_list def _get_header(self, msg_dict): try: content = self._get_header_field(msg_dict, 'From') sender = self._format_header_field('sender', content) except: sender = ('', '') try: content = self._get_header_field(msg_dict, 'Subject') except: content = _('No subject') try: subject = self._format_header_field('subject', content) except: subject = '' try: content = self._get_header_field(msg_dict, 'Date') datetime = self._format_header_field('date', content) except: logging.warning('Email date set to zero.') datetime = 0 try: msgid = self._get_header_field(msg_dict, 'Message-ID') except: msgid = '' return (sender, subject, datetime, msgid) def _get_header_field(self, msg_dict, key): if key in msg_dict: value = msg_dict[key] elif key.lower() in msg_dict: value = msg_dict[key.lower()] else: logging.debug("Couldn't get %s from message." % key) raise KeyError return value # format sender, date, subject etc. def _format_header_field(self, field_name, content): if field_name == 'sender': # get the two parts of the sender sender_real, sender_addr = email.utils.parseaddr(content) sender_real = self._convert(sender_real) sender_addr = self._convert(sender_addr) # create decoded tupel return (sender_real, sender_addr) if field_name == 'date': # make a 10-tupel (UTC) parsed_date = email.utils.parsedate_tz(content) # convert 10-tupel to seconds incl. timezone shift return email.utils.mktime_tz(parsed_date) if field_name == 'subject': return self._convert(content) # decode and concatenate multi-coded header parts def _convert(self, raw_content): # replace newline by space content = raw_content.replace('\n',' ') # workaround a bug in email.header.decode_header() content = content.replace('?==?','?= =?') # list of (text_part, charset) tupels tupels = decode_header(content) content_list = [] # iterate trough parts for text, charset in tupels: # set default charset for decoding if charset == None: charset = 'latin-1' # replace non-decodable chars with 'nothing' content_list.append(text.decode(charset, 'ignore')) # insert blanks between parts decoded_content = u' '.join(content_list) # get rid of whitespace decoded_content = decoded_content.strip() return decoded_content def _get_id(self, msgid, acc, folder, sender, subject, datetime): if len(msgid) > 0: id = hashlib.md5(msgid.encode('utf-8')).hexdigest() else: # Fallback ID. # Note: mails received on the same server, # in the same folder with identical sender and # subject but *no datetime* will have the same hash id, # i.e. only the first mail is notified. # (Should happen very rarely). id = hashlib.md5((acc.server + folder + acc.user + sender[1] + subject + str(datetime)) .encode('utf-8')).hexdigest() return id # # MailSyncer class # class MailSyncer: def __init__(self, cfg): self._cfg = cfg self._mails_by_account = {} self._mail_list = [] def sync(self, accounts): needs_rebuild = False # collect mails from given accounts rcv_lst = MailCollector(self._cfg, accounts).collect_mail(sort = False) # group received mails by account tmp = {} for acc in accounts: tmp[acc.get_id()] = {} for mail in rcv_lst: tmp[mail.account_id][mail.id] = mail # compare current mails against received mails # and remove those that are gone (probably opened in mail client). for acc_id in self._mails_by_account.iterkeys(): if acc_id in tmp: del_ids = [] for mail_id in self._mails_by_account[acc_id].iterkeys(): if not (mail_id in tmp[acc_id]): del_ids.append(mail_id) needs_rebuild = True for mail_id in del_ids: del self._mails_by_account[acc_id][mail_id] # compare received mails against current mails # and add new mails. for acc_id in tmp: if not (acc_id in self._mails_by_account): self._mails_by_account[acc_id] = {} for mail_id in tmp[acc_id]: if not (mail_id in self._mails_by_account[acc_id]): self._mails_by_account[acc_id][mail_id] = tmp[acc_id][mail_id] needs_rebuild = True # rebuild and sort mail list if needs_rebuild: self._mail_list = [] for acc_id in self._mails_by_account: for mail_id in self._mails_by_account[acc_id]: self._mail_list.append(self._mails_by_account[acc_id][mail_id]) self._mail_list = sort_mails(self._mail_list, sort_desc = True) return self._mail_list # # sort_mails function # def sort_mails(mail_list, sort_desc = False): sort_list = [] for mail in mail_list: sort_list.append([mail.datetime, mail]) # sort asc sort_list.sort() if sort_desc: sort_list.reverse() # recreate mail_list mail_list = [] for mail in sort_list: mail_list.append(mail[1]) return mail_list # # Memorizer class # class Memorizer(dict): def __init__(self): dict.__init__(self) self._changed = False def load(self): self.clear() self._changed = False # load last known messages from mailnag.dat dat_file = os.path.join(cfg_folder, 'mailnag.dat') if os.path.exists(dat_file): with open(dat_file, 'r') as f: for line in f: # remove CR at the end stripedline = line.strip() # get all items from one line in a list: [mailid, seen_flag] pair = stripedline.split(',') # add to dict [id : flag] self[pair[0]] = pair[1] # save mail ids to a file def save(self, force = False): if (not self._changed) and (not force): return if not os.path.exists(cfg_folder): os.makedirs(cfg_folder) dat_file = os.path.join(cfg_folder, 'mailnag.dat') with open(dat_file, 'w') as f: for id, seen_flag in self.items(): line = id + ',' + seen_flag + '\n' f.write(line) self._changed = False def sync(self, mail_list): for m in mail_list: if not m.id in self: # new mail is not yet known to the memorizer self[m.id] = '0' self._changed = True for id in self.keys(): found = False for m in mail_list: if id == m.id: found = True # break inner for loop break if not found: del self[id] self._changed = True # check if mail id is in the memorizer list def contains(self, id): return (id in self) # set seen flag for this email def set_to_seen(self, id): self[id] = '1' self._changed = True def is_unseen(self, id): if id in self: flag = self[id] return (flag == '0') else: return True mailnag-1.2.1/Mailnag/plugins/000077500000000000000000000000001272232533200161555ustar00rootroot00000000000000mailnag-1.2.1/Mailnag/plugins/dbusplugin.py000066400000000000000000000116061272232533200207070ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # dbusplugin.py # # Copyright 2013 - 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import dbus import dbus.service from Mailnag.common.dist_cfg import DBUS_BUS_NAME, DBUS_OBJ_PATH from Mailnag.common.plugins import Plugin, HookTypes from Mailnag.common.exceptions import InvalidOperationException from Mailnag.common.i18n import _ plugin_defaults = {} class DBusPlugin(Plugin): def __init__(self): self._dbusservice = None self._mails_added_hook = None self._mails_removed_hook = None def enable(self): controller = self.get_mailnag_controller() hooks = controller.get_hooks() self._dbusservice = DBusService(controller) def mails_added_hook(new_mails, all_mails): conv_new_mails = self._convert_mails(new_mails) conv_all_mails = self._convert_mails(all_mails) self._dbusservice.set_mails(conv_all_mails) self._dbusservice.MailsAdded(conv_new_mails, conv_all_mails) def mails_removed_hook(remaining_mails): conv_remaining_mails = self._convert_mails(remaining_mails) self._dbusservice.set_mails(conv_remaining_mails) self._dbusservice.MailsRemoved(conv_remaining_mails) self._mails_added_hook = mails_added_hook self._mails_removed_hook = mails_removed_hook hooks.register_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) hooks.register_hook_func(HookTypes.MAILS_REMOVED, self._mails_removed_hook) def disable(self): self._dbusservice = None controller = self.get_mailnag_controller() hooks = controller.get_hooks() if self._mails_added_hook != None: hooks.unregister_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) self._mails_added_hook = None if self._mails_removed_hook != None: hooks.unregister_hook_func(HookTypes.MAILS_REMOVED, self._mails_removed_hook) self._mails_removed_hook = None def get_manifest(self): return (_("DBus Service"), _("Exposes Mailnag's functionality via a DBus service."), "1.2", "Patrick Ulbrich ", True) def get_default_config(self): return plugin_defaults def has_config_ui(self): return False def get_config_ui(self): return None def load_ui_from_config(self, config_ui): pass def save_ui_to_config(self, config_ui): pass def _convert_mails(self, mails): converted_mails = [] for m in mails: d = {} name, addr = m.sender d['datetime'] = m.datetime # int32 (i) d['subject'] = m.subject # string (s) d['sender_name'] = name # string (s) d['sender_addr'] = addr # string (s) d['account_name'] = m.account_name # string (s) d['id'] = m.id # string (s) converted_mails.append(d) return converted_mails # DBUS server that exports Mailnag signals and methods class DBusService(dbus.service.Object): def __init__(self, mailnag_controller): self._mails = [] self._mailnag_controller = mailnag_controller bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus = dbus.SessionBus()) dbus.service.Object.__init__(self, bus_name, DBUS_OBJ_PATH) def set_mails(self, mails): self._mails = mails @dbus.service.signal(dbus_interface = DBUS_BUS_NAME, signature = 'aa{sv}aa{sv}') def MailsAdded(self, new_mails, all_mails): pass @dbus.service.signal(dbus_interface = DBUS_BUS_NAME, signature = 'aa{sv}') def MailsRemoved(self, remaining_mails): pass @dbus.service.method(dbus_interface = DBUS_BUS_NAME, out_signature = 'aa{sv}') def GetMails(self): return self._mails @dbus.service.method(dbus_interface = DBUS_BUS_NAME, out_signature = 'u') def GetMailCount(self): return len(self._mails) @dbus.service.method(dbus_interface = DBUS_BUS_NAME) def Shutdown(self): try: self._mailnag_controller.shutdown() except InvalidOperationException: pass @dbus.service.method(dbus_interface = DBUS_BUS_NAME) def CheckForMails(self): try: self._mailnag_controller.check_for_mails() except InvalidOperationException: pass @dbus.service.method(dbus_interface = DBUS_BUS_NAME, in_signature = 's') def MarkMailAsRead(self, mail_id): self._mails = filter(lambda m: m['id'] != mail_id, self._mails) try: self._mailnag_controller.mark_mail_as_read(mail_id) except InvalidOperationException: pass mailnag-1.2.1/Mailnag/plugins/libnotifyplugin.py000066400000000000000000000256701272232533200217570ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # libnotifyplugin.py # # Copyright 2013 - 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('Notify', '0.7') gi.require_version('GLib', '2.0') gi.require_version('Gtk', '3.0') import os import dbus import threading from gi.repository import Notify, Gio, Gtk from Mailnag.common.plugins import Plugin, HookTypes from Mailnag.common.i18n import _ from Mailnag.common.subproc import start_subprocess from Mailnag.common.exceptions import InvalidOperationException from Mailnag.daemon.mails import sort_mails NOTIFICATION_MODE_COUNT = '0' NOTIFICATION_MODE_SHORT_SUMMARY = '3' NOTIFICATION_MODE_SUMMARY = '1' NOTIFICATION_MODE_SINGLE = '2' plugin_defaults = { 'notification_mode' : NOTIFICATION_MODE_SHORT_SUMMARY, 'max_visible_mails' : '10' } class LibNotifyPlugin(Plugin): def __init__(self): # dict that tracks all notifications that need to be closed self._notifications = {} self._initialized = False self._lock = threading.Lock() self._notification_server_wait_event = threading.Event() self._notification_server_ready = False self._is_gnome = False self._mails_added_hook = None def enable(self): self._max_mails = int(self.get_config()['max_visible_mails']) self._notification_server_wait_event.clear() self._notification_server_ready = False self._notifications = {} # initialize Notification if not self._initialized: Notify.init("Mailnag") self._is_gnome = os.environ.has_key('GDMSESSION') and \ (os.environ['GDMSESSION'] == 'gnome') self._initialized = True def mails_added_hook(new_mails, all_mails): self._notify_async(new_mails, all_mails) self._mails_added_hook = mails_added_hook controller = self.get_mailnag_controller() hooks = controller.get_hooks() hooks.register_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) def disable(self): controller = self.get_mailnag_controller() hooks = controller.get_hooks() if self._mails_added_hook != None: hooks.unregister_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) self._mails_added_hook = None # Abort possible notification server wait self._notification_server_wait_event.set() # Close all open notifications # (must be called after _notification_server_wait_event.set() # to prevent a possible deadlock) self._close_notifications() def get_manifest(self): return (_("LibNotify Notifications"), _("Shows a popup when new mails arrive."), "1.1.1", "Patrick Ulbrich ", False) def get_default_config(self): return plugin_defaults def has_config_ui(self): return True def get_config_ui(self): radio_mapping = [ (NOTIFICATION_MODE_COUNT, Gtk.RadioButton(label = _('Count of new mails'))), (NOTIFICATION_MODE_SHORT_SUMMARY, Gtk.RadioButton(label = _('Short summary of new mails'))), (NOTIFICATION_MODE_SUMMARY, Gtk.RadioButton(label = _('Detailed summary of new mails'))), (NOTIFICATION_MODE_SINGLE, Gtk.RadioButton(label = _('One notification per new mail'))) ] box = Gtk.Box() box.set_spacing(12) box.set_orientation(Gtk.Orientation.VERTICAL) label = Gtk.Label() label.set_markup('%s' % _('Notification mode:')) label.set_alignment(0.0, 0.0) box.pack_start(label, False, False, 0) inner_box = Gtk.Box() inner_box.set_spacing(6) inner_box.set_orientation(Gtk.Orientation.VERTICAL) last_radio = None for m, r in radio_mapping: if last_radio != None: r.join_group(last_radio) inner_box.pack_start(r, False, False, 0) last_radio = r alignment = Gtk.Alignment() alignment.set_padding(0, 6, 18, 0) alignment.add(inner_box) box.pack_start(alignment, False, False, 0) box._radio_mapping = radio_mapping return box def load_ui_from_config(self, config_ui): config = self.get_config() radio = [ r for m, r in config_ui._radio_mapping if m == config['notification_mode'] ][0] radio.set_active(True) def save_ui_to_config(self, config_ui): config = self.get_config() mode = [ m for m, r in config_ui._radio_mapping if r.get_active() ] [0] config['notification_mode'] = mode def _notify_async(self, new_mails, all_mails): def thread(): with self._lock: # The desktop session may have started Mailnag # before the libnotify dbus daemon. if not self._notification_server_ready: if not self._wait_for_notification_server(): return self._notification_server_ready = True config = self.get_config() if config['notification_mode'] == NOTIFICATION_MODE_COUNT: self._notify_count(len(all_mails)) elif config['notification_mode'] == NOTIFICATION_MODE_SHORT_SUMMARY: self._notify_short_summary(new_mails, all_mails) elif config['notification_mode'] == NOTIFICATION_MODE_SUMMARY: self._notify_summary(new_mails, all_mails) else: self._notify_single(new_mails) t = threading.Thread(target = thread) t.start() def _notify_short_summary(self, new_mails, all_mails): summary = "" body = "" lst = [] mails = self._prepend_new_mails(new_mails, all_mails) mail_count = len(mails) if len(self._notifications) == 0: self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning i = 0 n = 0 while (n < 3) and (i < mail_count): s = self._get_sender(mails[i]) if s not in lst: lst.append(s) n += 1 i += 1 if self._is_gnome: senders = "%s" % ", ".join(lst) else: senders = ", ".join(lst) if mail_count > 1: summary = _("{0} new mails").format(str(mail_count)) if (mail_count - i) > 1: body = _("from {0} and others.").format(senders) else: body = _("from {0}.").format(senders) else: summary = _("New mail") body = _("from {0}.").format(senders) self._notifications['0'].update(summary, body, "mail-unread") self._notifications['0'].show() def _notify_summary(self, new_mails, all_mails): summary = "" body = "" mails = self._prepend_new_mails(new_mails, all_mails) if len(self._notifications) == 0: self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning ubound = len(mails) if len(mails) <= self._max_mails else self._max_mails for i in range(ubound): if self._is_gnome: body += "%s:\n%s\n\n" % (self._get_sender(mails[i]), mails[i].subject) else: body += "%s - %s\n" % (ellipsize(self._get_sender(mails[i]), 20), ellipsize(mails[i].subject, 20)) if len(mails) > self._max_mails: if self._is_gnome: body += "%s" % _("(and {0} more)").format(str(len(mails) - self._max_mails)) else: body += _("(and {0} more)").format(str(len(mails) - self._max_mails)) if len(mails) > 1: # multiple new emails summary = _("{0} new mails").format(str(len(mails))) else: summary = _("New mail") self._notifications['0'].update(summary, body, "mail-unread") self._notifications['0'].show() def _notify_single(self, mails): # In single notification mode new mails are # added to the *bottom* of the notification list. mails = sort_mails(mails, sort_desc = False) for mail in mails: n = self._get_notification(self._get_sender(mail), mail.subject, "mail-unread") notification_id = str(id(n)) if self._is_gnome: n.add_action("mark-as-read", _("Mark as read"), self._notification_action_handler, (mail, notification_id)) n.show() self._notifications[notification_id] = n def _notify_count(self, count): if len(self._notifications) == 0: self._notifications['0'] = self._get_notification(" ", None, None) # empty string will emit a gtk warning if count > 1: # multiple new emails summary = _("{0} new mails").format(str(count)) else: summary = _("New mail") self._notifications['0'].update(summary, None, "mail-unread") self._notifications['0'].show() def _close_notifications(self): with self._lock: for n in self._notifications.itervalues(): n.close() self._notifications = {} def _get_notification(self, summary, body, icon): n = Notify.Notification.new(summary, body, icon) n.set_category("email") n.set_hint_string("desktop-entry", "mailnag") if self._is_gnome: n.add_action("default", "default", self._notification_action_handler, None) return n def _wait_for_notification_server(self): bus = dbus.SessionBus() while not bus.name_has_owner('org.freedesktop.Notifications'): self._notification_server_wait_event.wait(5) if self._notification_server_wait_event.is_set(): return False return True def _notification_action_handler(self, n, action, user_data): with self._lock: if action == "default": mailclient = get_default_mail_reader() if mailclient != None: start_subprocess(mailclient) # clicking the notification bubble has closed all notifications # so clear the reference array as well. self._notifications = {} elif action == "mark-as-read": controller = self.get_mailnag_controller() try: controller.mark_mail_as_read(user_data[0].id) except InvalidOperationException: pass # clicking the action has closed the notification # so remove its reference. del self._notifications[user_data[1]] def _get_sender(self, mail): name, addr = mail.sender if len(name) > 0: return name else: return addr def _prepend_new_mails(self, new_mails, all_mails): # The mail list (all_mails) is sorted by date (mails with most recent # date on top). New mails with no date or older mails that come in # delayed won't be listed on top. So if a mail with no or an older date # arrives, it gives the impression that the top most mail (i.e. the mail # with the most recent date) is re-notified. # To fix that, simply put new mails on top explicitly. return new_mails + [m for m in all_mails if m not in new_mails] def get_default_mail_reader(): mail_reader = None app_info = Gio.AppInfo.get_default_for_type ("x-scheme-handler/mailto", False) if app_info != None: executable = Gio.AppInfo.get_executable(app_info) if (executable != None) and (len(executable) > 0): mail_reader = executable return mail_reader def ellipsize(str, max_len): if max_len < 3: max_len = 3 if len(str) <= max_len: return str else: return str[0:max_len - 3] + '...' mailnag-1.2.1/Mailnag/plugins/soundplugin.py000066400000000000000000000057051272232533200211050ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # soundplugin.py # # Copyright 2013 - 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('Gst', '1.0') import os import threading from gi.repository import Gst from Mailnag.common.plugins import Plugin, HookTypes from Mailnag.common.utils import get_data_file from Mailnag.common.i18n import _ plugin_defaults = { 'soundfile' : 'mailnag.ogg' } class SoundPlugin(Plugin): def __init__(self): self._mails_added_hook = None def enable(self): def mails_added_hook(new_mails, all_mails): config = self.get_config() gstplay(get_data_file(config['soundfile'])) self._mails_added_hook = mails_added_hook controller = self.get_mailnag_controller() hooks = controller.get_hooks() hooks.register_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) def disable(self): controller = self.get_mailnag_controller() hooks = controller.get_hooks() if self._mails_added_hook != None: hooks.unregister_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) self._mails_added_hook = None def get_manifest(self): return (_("Sound Notifications"), _("Plays a sound when new mails arrive."), "1.1", "Patrick Ulbrich ", False) def get_default_config(self): return plugin_defaults def has_config_ui(self): return False def get_config_ui(self): # TODO : Add ui to specify the path # of a custom sound file. return None def load_ui_from_config(self, config_ui): pass def save_ui_to_config(self, config_ui): pass class _GstPlayThread(threading.Thread): def __init__(self, ply): self.ply = ply threading.Thread.__init__(self) def run(self): def on_eos(bus, msg): # loggin.debug('EOS') self.ply.set_state(Gst.State.NULL) return True bus = self.ply.get_bus() bus.add_signal_watch() bus.connect('message::eos', on_eos) self.ply.set_state(Gst.State.PLAYING) _gst_initialized = False def gstplay(filename): global _gst_initialized if not _gst_initialized: Gst.init(None) _gst_initialized = True try: ply = Gst.ElementFactory.make("playbin", "player") ply.set_property("uri", "file://" + os.path.abspath(filename)) pt = _GstPlayThread(ply) pt.start() except: pass mailnag-1.2.1/Mailnag/plugins/spamfilterplugin.py000066400000000000000000000075061272232533200221240ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # spamfilterplugin.py # # Copyright 2013 - 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from Mailnag.common.plugins import Plugin, HookTypes from Mailnag.common.i18n import _ plugin_defaults = { 'filter_text' : 'newsletter, viagra' } class SpamfilterPlugin(Plugin): def __init__(self): self._filter_mails_hook = None self._filter_list = None def enable(self): config = self.get_config() self._filter_list = config['filter_text'].replace('\n', '').split(',') def filter_mails_hook(mails): lst = [] for m in mails: if not self._is_filtered(m): lst.append(m) return lst self._filter_mails_hook = filter_mails_hook controller = self.get_mailnag_controller() hooks = controller.get_hooks() hooks.register_hook_func(HookTypes.FILTER_MAILS, self._filter_mails_hook) def disable(self): controller = self.get_mailnag_controller() hooks = controller.get_hooks() if self._filter_mails_hook != None: hooks.unregister_hook_func(HookTypes.FILTER_MAILS, self._filter_mails_hook) self._filter_mails_hook = None self._filter_list = None def get_manifest(self): return (_("Spam Filter"), _("Filters out unwanted mails."), "1.1", "Patrick Ulbrich ", False) def get_default_config(self): return plugin_defaults def has_config_ui(self): return True def get_config_ui(self): box = Gtk.Box() box.set_spacing(12) box.set_orientation(Gtk.Orientation.VERTICAL) #box.set_size_request(100, -1) desc = _('Mailnag will ignore mails containing at least one of \nthe following words in subject or sender.') label = Gtk.Label(desc) label.set_line_wrap(True) #label.set_size_request(100, -1); box.pack_start(label, False, False, 0) scrollwin = Gtk.ScrolledWindow() scrollwin.set_shadow_type(Gtk.ShadowType.IN) scrollwin.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrollwin.set_size_request(-1, 60) txtbuffer = Gtk.TextBuffer() txtview = Gtk.TextView() txtview.set_buffer(txtbuffer) txtview.set_wrap_mode(Gtk.WrapMode.WORD) scrollwin.add(txtview) box.pack_start(scrollwin, True, True, 0) return box def load_ui_from_config(self, config_ui): config = self.get_config() txtview = config_ui.get_children()[1].get_child() txtview.get_buffer().set_text(config['filter_text']) def save_ui_to_config(self, config_ui): config = self.get_config() txtview = config_ui.get_children()[1].get_child() txtbuffer = txtview.get_buffer() start, end = txtbuffer.get_bounds() config['filter_text'] = txtbuffer.get_text(start, end, False) def _is_filtered(self, mail): is_filtered = False for f in self._filter_list: # remove CR and white space f = f.strip() if len (f) == 0: continue f = f.lower() sender_name, sender_addr = mail.sender if (f in sender_name.lower()) or (f in sender_addr.lower()) \ or (f in mail.subject.lower()): # sender or subject contains filter string is_filtered = True break return is_filtered mailnag-1.2.1/Mailnag/plugins/userscriptplugin.py000066400000000000000000000072161272232533200221570ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # userscriptplugin.py # # Copyright 2013 - 2016 Patrick Ulbrich # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('Gtk', '3.0') import os from gi.repository import Gtk from Mailnag.common.plugins import Plugin, HookTypes from Mailnag.common.i18n import _ from Mailnag.common.subproc import start_subprocess plugin_defaults = { 'script_file' : '' } class UserscriptPlugin(Plugin): def __init__(self): self._mails_added_hook = None def enable(self): def mails_added_hook(new_mails, all_mails): self._run_userscript(new_mails) self._mails_added_hook = mails_added_hook controller = self.get_mailnag_controller() hooks = controller.get_hooks() hooks.register_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) def disable(self): controller = self.get_mailnag_controller() hooks = controller.get_hooks() if self._mails_added_hook != None: hooks.unregister_hook_func(HookTypes.MAILS_ADDED, self._mails_added_hook) self._mails_added_hook = None def get_manifest(self): return (_("User Script"), _("Runs an user defined script on mail arrival."), "1.2", "Patrick Ulbrich ", False) def get_default_config(self): return plugin_defaults def has_config_ui(self): return True def get_config_ui(self): box = Gtk.Box() box.set_spacing(12) box.set_orientation(Gtk.Orientation.VERTICAL) #box.set_size_request(100, -1) markup_str = "<%s> <%s> <%s>" % (_('account'), _('sender'), _('subject')) desc = _( "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences.") % markup_str label = Gtk.Label() label.set_line_wrap(True) label.set_markup(desc) #label.set_size_request(100, -1); box.pack_start(label, False, False, 0) filechooser = Gtk.FileChooserButton() box.pack_start(filechooser, True, True, 0) return box def load_ui_from_config(self, config_ui): config = self.get_config() script_file = config['script_file'] if len(script_file) > 0: filechooser = config_ui.get_children()[1] filechooser.set_filename(script_file) def save_ui_to_config(self, config_ui): config = self.get_config() filechooser = config_ui.get_children()[1] script_file = filechooser.get_filename() if script_file == None: script_file = '' config['script_file'] = script_file def _run_userscript(self, new_mails): config = self.get_config() script_file = config['script_file'].strip() if (len(script_file) > 0) and os.path.exists(script_file): script_args = [ script_file, str(len(new_mails)) ] for m in new_mails: sender_name, sender_addr = m.sender if len(sender_addr) == 0: sender_addr = 'UNKNOWN_SENDER' script_args.append(m.account_name) script_args.append(sender_addr) script_args.append(m.subject) start_subprocess(script_args) mailnag-1.2.1/NEWS000066400000000000000000000134341272232533200136300ustar00rootroot00000000000000Version 1.2.1 (2016-05-28): =========================== * Improved GNOME-Software integration * Fixed missing plugin config dialogs in Ubuntu 14.04 * Fixed IMAP idle folders issue * Fixed GTK warnings * Misc other fixes and improvements * Updated translations Version 1.2.0 (2016-03-23): =========================== * Added support for notification settings in GNOME-Control-Center * Improved account dialog featuring a new IMAP folder chooser * libnotify plugin: added new 'Short Summary' notification mode (new default notification mode; recommended for GNOME 3) * userscript plugin: accountnames are now passed to the specified userscript * Non-SSL accounts are now utilizing STARTTLS if available (thanks todi!) * Mailnag now logs a warning if an unencrypted connection is detected * Acountnames are now exported via the DBUS-API * The config window is resizable now * Misc other fixes and improvements * Updated translations / removed badly maintained translations NOTES: If you're using the userscript plugin, please note that Mailnag now passes (accountname, sender, subject) sequences to the script as opposed to (sender, subject) sequences in previous releases. Version 1.1.0 (2015-02-08): =========================== * GNOME Online Accounts integration (via a plugin -> https://github.com/pulb/mailnag-goa-plugin) * Added support for platform-specific credential (login password) backends (Configurable in the config file. Supported options are currently "AUTO", "GNOME" and "NONE".) * The GNOME-Keyring dependency is optional now * Improved connectivity tests (Configurable in the config file. Supported options are "AUTO", "NETWORKMANAGER" and "PING".) * Shorter notification strings * Added appdata file for GNOME Software app * Removed bash wrapper scripts * Misc other fixes and improvements * Updated translations IMPORTANT NOTES FOR PACKAGERS: Please note that GNOME-Keyring and Networkmanager are now optional dependencies and the httplib2 dependency is not required anymore. Version 1.0.0 (2014-06-28): =========================== * Mailnag is desktop-independent now (was GNOME3-only) * Added plugin system to allow easy extensibility * Cut down the core daemon functionality to mail checking only and moved everything else to plugins * Released GNOME 3/Ubuntu Unity extension/plugin for a tighter desktop integration * Added account assistants for popular email providers like Gmail * Added DBUS service (for remote control/integration in other apps) * Redesigned and simplified config UI * Added propper logging (log messages are sent to stdout and the system log now) * Heavy code cleanup & refactoring * Reduced disk write access, other performance improvements * Fixes for various major and minor bugs * New icon by Reda Lazri IMPORTANT NOTES: The fileformat of the config file has changed. It's highly recommended to rename/backup your current config file (~/.config/mailnag/mailnag.cfg). Version 0.5.2 (2013-01-06): =========================== * Fixed path of the autstart file * Minor other fixes Version 0.5.1 (2012-12-23): =========================== * Restored translated strings (previously available in mailnag <= 0.4.3) that were removed due to a bug in the gen_po_template script * Fixed a race condition that can lead to mutliple mailnag instances Version 0.5.0 (2012-12-05): =========================== * Bugfix: (really) don't crash on session start if the notification DBUS interface isn't available yet * Migrated to the new keyring gir binding * Removed evolution account import (not working anymore) * Minor other fixes * Updated translations IMPORTANT NOTES: - Packagers should incorporate the new dependecy list. - The new keyring binding stores credentials in ~/.local/share/keyrings instead of in ~/.gnome2/keyrings. So you probably have to fire up mailnag_config and re-enter your mail account password(s). Version 0.4.4 (2012-10-20): =========================== * Bugfix: don't crash on session start if the notification DBUS interface isn't available yet * Bugfix: fix notification sound playback in GNOME 3.6 * Removed messagetray-label configuration since gnome-shell no longer shows labels in the messagetray Version 0.4.3 (2012-09-22): =========================== * Added installation script (setup.py) * IMAP related bugfixes * Updated translations Version 0.4.2 (2012-07-10): =========================== * Bugfix: enable gettext fallback language * Updated translations Version 0.4.1 (2012-05-21): =========================== * Fixed some crashes and connection issues * Use unicode for translated strings * Don't play notification sounds when GNOME Shell notifications are disabled * Updated translations Version 0.4 (2012-01-15): ========================= * Much improved IMAP IDLE support * Reconnect if a connection has been lost (e.g. after standby) * Use GNOMEs default mail client * Enable SSL by default * Use a meaningful messagetray label by default * Added version info to the about tab * Refactoring, removed unused code * Updated translations * Bugfixes Version 0.3 (2011-11-27): ========================= * Support for IMAP-Push notifications (thanks tony747!) * Single/summary notification modes * Support for GNOME 3.2 notification counters (single mode only) * Mails can be marked as read * Explicit SSL encryption * Autostart in GNOME sessions only * Detection of default email client * Notification sound playback via GStreamer (ogg) * Removed GTK2 workaround code * Lots of bugfixes, rewritten code and refactoring * New translations Please note that this release breaks existing config files (for the first and last time), so make sure to delete ~/.config/mailnag/mailnag.cfg before upgrading. Version 0.2 (2011-10-17): ========================= * Added many new translations * Bugfixes Version 0.1 (2011-07-06): ========================= * Initial release mailnag-1.2.1/README.md000066400000000000000000000076301272232533200144110ustar00rootroot00000000000000# Mailnag An extensible mail notification daemon. Mailnag is a daemon program that checks POP3 and IMAP servers for new mail. On mail arrival it performs various actions provided by plugins. Mailnag comes with a set of desktop-independent default plugins for visual/sound notifications, script execution etc. and can be extended with additional plugins easily. __This project needs your support!__ If you like Mailnag, please help to keep it going by [contributing code](https://github.com/pulb/mailnag), [reporting/fixing bugs](https://github.com/pulb/mailnag/issues), [translating strings into your native language](https://translations.launchpad.net/mailnag), [writing docs](https://github.com/pulb/mailnag/wiki) or by [making a donation](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8F5FNJ3U4N7AW). PayPal — The safer, easier way to pay online. ## Installation ### Ubuntu Mailnag has an official [Ubuntu PPA](https://launchpad.net/~pulb/+archive/mailnag). Issue the following commands in a terminal to enable the PPA and install Mailnag. sudo add-apt-repository ppa:pulb/mailnag sudo apt-get update sudo apt-get install mailnag As of Ubuntu 13.04 (Raring), Mailnag is also available in the official repos. Run `sudo apt-get install mailnag` in a terminal to install it. ### Debian Mailnag is currently available in Debian unstable. Run `sudo apt-get install mailnag` in a terminal to install it. ### Fedora As of Fedora 17, Mailnag is available in the official Fedora repos. Just run `yum install mailnag` (as root) in a terminal to install the package. ### Arch Linux Mailnag is available in the [AUR](https://aur.archlinux.org/packages/mailnag/) repository. Please either run `yaourt -S mailnag` or `packer -S mailnag` (as root) to install the package. ### Generic Tarballs Distribution independent tarball releases are available [here](https://launchpad.net/mailnag/trunk/mailnag-master). Just run `./setup.py install` (as root) to install Mailnag, though make sure the requirements stated below are met. ###### Requirements * python2 (python3 won't work!) * pygobject * gir-notify (>= 0.7.6) * gir-gtk-3.0 * gir-gdkpixbuf-2.0 * gir-glib-2.0 * gir-gst-plugins-base-1.0 * python-dbus * pyxdg * gettext * gir-gnomekeyring-1.0 (optional) * networkmanager (optional) ## Configuration Run `mailnag-config` to setup Mailnag. Closing the configuration window will start Mailnag automatically. ### Default Mail Client Clicking a mail notification popup will open the default mail client specified in `GNOME Control Center -> Details -> Default Applications`. If you're a webmail (e.g. gmail) user and want your account to be launched in a browser, please install a tool like [gnome-gmail](http://gnome-gmail.sourceforge.net). ### Desktop Integration By default, Mailnag emits libnotify notifications, which work fine on most desktop environments but are visible for a few seconds only. If you like to have a tighter desktop integration (e.g. a permanently visible indicator in your top panel) you have to install an appropriate extension/plugin for your desktop shell. Currently the following desktop shells are supported: * GNOME-Shell ([GNOME-Shell extension](https://github.com/pulb/mailnag-gnome-shell)) * Ubuntu Unity ([MessagingMenu plugin](https://github.com/pulb/mailnag-unity-plugin)) * Cinnamon ([Applet by hyOzd](https://bitbucket.org/hyOzd/mailnagapplet)) Furthermore, I highly recommend GNOME users to install the [GOA plugin](https://github.com/pulb/mailnag-goa-plugin), which makes Mailnag aware of email accounts specified in GNOME Online Accounts. ## Screenshots ![Screenshot](https://raw.githubusercontent.com/pulb/mailnag-design/master/Flyer/Mailnag_flyer2.png) mailnag-1.2.1/data/000077500000000000000000000000001272232533200140355ustar00rootroot00000000000000mailnag-1.2.1/data/account_dialog.ui000066400000000000000000000342641272232533200173600ustar00rootroot00000000000000 False 5 Mail Account False True dialog False vertical 18 False end gtk-cancel True True True True True True 0 gtk-ok True False True True True True True 1 False True end 0 True False 6 6 6 6 6 True True True number 1 5 True True True 1 4 True True True False 1 3 True True True 1 2 True True True 1 1 Enable Push-IMAP True True False 0 True 0 6 2 True False 1 0 Enable SSL encryption True True False 0 True 0 7 2 True False Accountname: 0 0 1 True False Account type: 0 0 0 True False User: 0 0 2 True False Password: 0 0 3 True False Server: 0 0 4 True False Port: 0 0 5 True False True True False True True in 100 True True liststore_folders False True False Folders (optional) 0 8 2 True True 1 button_cancel button_save mailnag-1.2.1/data/config_window.css000066400000000000000000000002261272232533200174030ustar00rootroot00000000000000.bg-gradient { background-image: linear-gradient(to bottom, @theme_bg_color, shade(@theme_bg_color, 0.94) 12%); } mailnag-1.2.1/data/config_window.ui000066400000000000000000000426071272232533200172410ustar00rootroot00000000000000 False Mailnag Configuration True False vertical True False 18 18 12 12 True General True True True False True 0 Accounts True True True False True 1 Plugins True True True False True 2 False True 0 True True False False True False True False 12 12 12 vertical 12 True False vertical False False 0 True False APP_DESC True False False 1 False False 0 True True <a href="https://github.com/pulb/mailnag">Mailnag</a> - An extensible mail notification daemon. Copyright (c) 2011 - 2016 Patrick Ulbrich and contributors. True False False 1 True True center False False 2 True False vertical True True in True True liststore_accounts False True False 1 True True 0 True False icons 1 True False Add Account list-add-symbolic False True True False Remove Account list-remove-symbolic False True True False Edit Account text-editor-symbolic False True False True 1 1 True False vertical True True in True True liststore_plugins False True False 1 True True 0 True False icons 1 True False Edit Plugin text-editor-symbolic False True False True 1 2 True True 1 mailnag-1.2.1/data/icons/000077500000000000000000000000001272232533200151505ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/000077500000000000000000000000001272232533200166075ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/128x128/000077500000000000000000000000001272232533200175445ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/128x128/apps/000077500000000000000000000000001272232533200205075ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/128x128/apps/mailnag.png000066400000000000000000000220651272232533200226320ustar00rootroot00000000000000PNG  IHDR>asBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATxy$}?飯ݙSmR+Xe1aBDZ$V#FvBcĐs  C@"HMI)ky}3Wzz힙ݥ=j{W}js v/wrL1!.DŽrL1!.DŽrL1!.DŽrL1!.DŽrL1!.DŽn`?A' AhlriG>{fw Nǩ}?Gnl@ 6!Bm[0˿#Ϗ% Ixax$d->K9^[0?,G{eߧ90 ^AXp(6Q'g(4ෑ:ܫ/MX.Asuǿե'p6BA~k\5A7\x70.᳟{ >i.YR ԛ'XıO>~qLpBWj{>Ky ,fl lnW43\ ؿs=M*Xk7č2V@c-BԧG;y_"%)wg;}5@ ]f6!pߑ |=sO$vƒP 9h=MB @@{ :"t r 0(<aSH1@S':8+W؄Q7W\-i:J;b+DMӡMAv \ٿ3O2;mhpq6řpHo^Hg>_j嫩R*#jJΉ>I>^Ա8曊#{Bf {:Ǚ) by-3Zᰵa AlzΩ_&o} #T߂Wdl>@O]e+́~uXG>?͏s ʦ!3/v9}=c:=chT6을VK=3@BSB痼_ qV#D5ր$7">}lD Z"Կ{&#Tї~txѭ^f1me._zJbJdi%xPy6ۮur:>ֹ ۛuZR *Y n >,,P&0Y36ۨQ5FYl ϝpv)CU`- >szQ{d70+r+ f<:s|O8q'xLT=˱6%7dZml2> At?z5O,rBkZ{! k{r:nsm?*%'̵ӗlźUV_oqfqst&{bRqMsWOw9*^ jՄ/1c'η2*Gqng8ͳ]skAPpt&d.YI3>'-{#{>gfH~s_#EN G>d>K|v9ɡo@""2+ecXI[bJrVXBs4_?Z,Ϟpn&CUB}{4w<9f*TNӷm GiGϐ4pITm@!Z 9Z7ѵ@(LۤbO6_Dr|k4 vg\\,rvbeH>|4jon塽Xy_ חB->Yܑf` DIAjL_jɍ! kR]#o~xcT֪5<'2(E/sr0Nĕ qqOCS51N O恹7!((ʭ[0ÇT5 iTu8C` 5 -ʼ8q, saLDaDZ:׆IPi@X&X[zͻGwZ'XyBW, N,$\\K6'(Xh|T֫~g'#IrXQE ד{H$t9'W~D ^@g2%37҇c͌֊( Aa )$ur`1G3v/_sn9cǪ쭮=N x gGF 02^}헙$"05ZIWZj\87 } r2|ڂ' L~㱏m>ޱD)R 4ZH!PJFkEJpT\9nb^]{"9Pِ<)~u;1R^RǎWߜ{?>e7~hl{3[ !0^,x]Ӑ>mн~%f . -0yr+kop,t刟|?C~Z,A&}8!Guϙ'4 KKG<{Gw Zy|w6s|R ˾H>z,Pd3L7:\T4Q"m7Ņ#Z.Seh,%װ{ ԩԥgov;[3A 3\+RB\ Qŵm;T3}|OB9*[}WNvYlwOGDگr?/,d|l~0N55Zb:EVCtSÂ!p2rEvlE'&sүj@ @ HM֚@kڠ={YV{1#9~Ŕ'WGkN{NHI9 8ܹfE{mt|Ư*:rВ,ND9 M̘R/B0 '(c\[?޿uW֊wL QxBf@h() mЁf!-SK\ I\Ȩ:^G+<8Q _:+ȭFq$bwm|baáBd@BA5Z)*E$+\Yq{6F |.VƷ竦XqnuOz(UŜH]E*UuW#N :*a q !v'qyHp"*GPjBm#<,DNTr>?(#z4+NRt3Ntx`6bBVLE']㥋 8ܴ4,"T`K(G1X85?>獫i嵀;r\U1Eu(3(A=t`o1Xbĉ'858!θO㵅"A+zZY|Dq~$ztP 5`UL4U% CJ 42#d'-p"1B 5 R;c6kZԐoࣅ G{z+JwDmZ]cGT*ĒO?\ z!jy"qWCx`XkQJ[ڙ./]H(rq?Ԫs @ihuB>OMA@x ʤ-!EkqqG^1 (L6PE'^ⲫs-c%e쥋 r>y_́#!"Lk~NX3j}'_)Ҋ+,ˌ,C6v+Z0ߐԳ+LeĵJQTQJ1%k\p[JUok{CHyZ+PM PIj4W{X6!7&y{#>x(PXW.^[3ȑ-/zg %q@]3eT:%j([,4Lr;0R6@qpG_Sވ_DDZ4Q#+p6\?aAyj§*)$ 4OmJ~C'S 8/&ծm͠e ^`%Q]VB/( ( {!\.CwA>!@݊ iS뷩,fNbzl>.&9B[/F:z{p[ `naՁ^Xk}Y!P$A@UeV)Kk797Q()];X}=N5U.ȡ #-"Tb/_du¾GQVzP6^ n`l:^OTUփβc`2Ŋ/,>A1A{X8EhXjPupq%Uyhh^ [djNkt0tt^b+\ *\\2T$}rBSoxm'u*;n( D+{#}f{jTGvH! pA_Yh9($;J+6aRD8vXkh0 hHӔ<7BUJ)1/7OyTe95W|XsUVp/D I<8RMاrj 9;;K$Qt: Rn ÈJeu D)K$(|BG)sGJIg:]* T%*i.sKƺ"\Dh3-͠Zz(Ŭl4Z; }9E#@FJ2~kV̈́L9a&I1i}s3!MWc<'M3sP3&o_Oirl޵![n 25(&~yZgjoO`iM\J{*E<$ nƘ=49KףYNs$I2AAmnvi,J)Agp YG) +Wp9}=]mt>"#>36 GFl3 g]QhJ# J^M@o.;I!@$T!~Z.YWldY^ kO68^LŒq0 &708Д;W +1{Ꜹ縢%Tٳ#g}f|)ѡ6SEi={Bzs}19GE(h}8N`p@4S'Vm~8ϲQ?sJ8" I2*;BE% 2cZ?aD^ζ,'>1jBI _̱iE#ުR -P"F͗i)$ly`7L(пݪ$)q ?˨*Z5S [tFQh8EEܣZq/vh'>[F %ՀGhe^ќBzhJ]Ϲ1(aiiRl믳UTY慹W5"},FJ#_97<\ efBPK.iŌS7Z;sߞl"J_5ʁ ,o!0&G)4VknI!@%΍}?Ð,a={C1A{Wޗ( 6֓L))'D9S+WfW.R~.s{с&PYyqV`If yYQ& {g r0:Q"Q4EEH3B69sy oV̚c 8+Χ}3SUI4ט+xYAXmo ɑJQ ܭN+Qenp#œ- %7nK%Z}e kQH'+ϰb(C{ST[WPbT`^ Ƴ߉:5aZ~%q 孷:Vn)6k-B H)j `'CU13lT͚%aJ2k@U8/. Vlb[$\I / r k77^! P".~ ڃqsnPXִlP~u+v$`B/ݐB^AImCmo&a!DTjq3v H`P*nZ1jJMe 7xdm.~mZY 4 ZX`'WmB٥SEU/|V¤T*EP : G ގBh?{w6ۍrq(h6kZGT n;ضPJ+Tk>CZ- ^)%Q$ z!hͰJ$pNl(wfoUHh6iZZ&/Ǵ (W)e>A4==Mn|_['|XVU>ßanZӂshl|gMpð<ϙV#XaMO05,5crZoVD,49Fn{HUUXc333hT*Z(*p;2w/pO)ƎdshI h'm͆HAPd뼣I(*j_VR K;np"M* %bW}IDATQo¢t + BJzv5xCr)MmP(gRPTpA*׫w}SZNVbMJU4 y6 sX/:J_kT1h 0ndYT8.sbˇV պ2QΡF1W^Ï9v,cxAeίz7Zp+(h6}?8 i6 OaJ۾1DBJAZ]"L2/ĕuɛʯ(L#pUP"[2l0P2ۖ/5K/SAY;nl(ei;˲5% >~4MIduɟii /\YO~ H+MS.\8u6o_ -`gcA:g,&鵋.槾S[[-G!@IPnChh, Z)|h3wohsNEntTYmo$|= (meU 7JޮvJ;uvscES?L&838 Gn[MNg촠Fΰl6#Mq+ ~X+I$٭-ޭZ<6DX;; vR #nH5$ c\aNq,|J0"` `}coح^fрψ0)jw|;ߍ*{`L9pt2.Dw:6Їe4.,mYb;w &;aƄrL1!.DŽrL1!.DŽrL1!.DŽrL1!.DŽrOIENDB`mailnag-1.2.1/data/icons/hicolor/16x16/000077500000000000000000000000001272232533200173745ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/16x16/apps/000077500000000000000000000000001272232533200203375ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/16x16/apps/mailnag.png000066400000000000000000000011331272232533200224530ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8œ1OSasrM6Xa0@Qč:999EI)9PM ޶>HH<>yXk8jNZz_lrzM *OJb!j`Aar>4=4|e jKRX~F5qZ^a ߖïXSDܻYɝYJ~\B6Шz.!n\G 3g혭k+*Q"QJ̨1 &Wi͘FJΏMQ WFtDj'HX [ hsH}AkxgtkQn E."4c(ލ8ZC0[pZN~jyDsj10a-?|ZƘq֚0B)y<\ ZcӖ$pIENDB`mailnag-1.2.1/data/icons/hicolor/22x22/000077500000000000000000000000001272232533200173665ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/22x22/apps/000077500000000000000000000000001272232533200203315ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/22x22/apps/mailnag.png000066400000000000000000000017201272232533200224470ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<MIDAT8͕k\E3gn65M$҄+EDZ/ ?_^yDJ7|شMRٳ'眙3dD }^f/;ό $ww.պkiVn\nyh_h/ʋ/ oa&dI$n|\:w,鏽BT@61)uUaecs3u/[oÅ6 \d_!Njd).ڰ%ֆpv,GsP6`x=,05~9889Xfw-^-_~FgJ\>@'loo@VfWcQ٨j- ǿf0L\vE0RdaG Xn}.Z \\Gv'?3gXzzK%E( V5X`u`ή;'#!,Nyfr!t i k-% s]&C?B[m"dn+|#&uH᝺@ϻhIRg)%b zevۚV@sP%%S8>):?Q jY ]Rc,X*GS]a`?kET*8"'\+mTqՁ{GpK'e$#i*VHRC^M0==Ǿm=)M=oeaf^>X63C>sǶB-QD S1XX/fX@U!-=Lݘv}?-=:5S>9|>~;_K|y .VW3eLߊscئ k-annNe :`H78_p5be|^x̉6Ω'e1ZQz/cae)"0sy~Hַ5aARtALjn$4A%O{h Z*\h\i&tScS1DZ;Fl7~7e1$:\HꃽFm|>϶LL9rPS0[jC?^]ŮY$&\1tǬRWP jucR3Ai!/i+X AHq28' ֽccc-G8ِtgSV. +l\~ W%zc)4[-aÆ $JU]HΰqbX#2Z\۶niIҔvM'͑$ C@W+14X,-**àVEm)˲n˘aJ%R^'A@E vh~?E0 ]ַm#g~7~Ab[]Ȯ3@yk@i @Ym4,6K|s3wFfkW_'p!Yi<'w^aK@D'5}x|`ogͿq:.Lv]/(|Z[&xM y5A"|Xuٔ~"e#l5wymX: 9 @D"~_226+f&r#=287cks09Zbקiyoas8Ϡ%1P+ޟX1ocg>W$!C\DS0dS 'cchږLǜ٥m<1]O1~7AdHm&+i?9N5QgÃ~$˿_7jݙݫlۘ .G{v|?#I?#RlTs.d:cAE?:=u(L[12Jv3sZ4s\)ĮvF!3guvpL͓pyN`rۛPѸq!) wjvx:zt3='&xr37/{g9:]{ʂa:̭J/r =mp3rY@\0GhL)lN_^jF/<]DgvylO6Ԏk_uz3M&Qy >J (!!}d{TG"+ 䈊nT*शص""_g^"%y(EG0IJ:e|Ee_#H{͐$P8@)3rŹKOѿRGR )j +,;K UM7";š= =cWBxpWP.=&66gE/rbg]*sIHhDB4뿋e`?'M %dv6QzBXrHFx Xkuf' `\x]o|j o=`HA{7? mfBo=d:Ǽ01|\:o``-b->eE 1X>&C<<:ߵ2WJ+6Q3q'0nqV=-L~nPf=(>"Sそ5;k:܁QXZES]wuyzf{!8Yi;,`z䡘&58kp ?Qh_eD+"9wWlw|wr L]FXCՈY{`׋V+qyTC:9_jsiϣ4sr9'.<{os.8&a12>B2N16é Ҝۧ|jIYU HUvVV)R˶I"g}Y`]i C1k63'7fF+.rǶ'hn^ϭv'[M[e{|ŻÌI~ώcmc+ w"b]yx/jDzgJ|6\+knJ [rƮ1WRl#o\l'[wLwWGlXo]SK7)OvSK|r'w/zsnUzzn[ XL)tG6e㔽jyQ433S#dml7,algВ!h jqp\\a61N:-j˘,c` lBǞY tp<#'L .|EN{̯=>"W}u?): :HZeDY^Fmg&?#jYP3 g~ypL[0A$|SۿAGPR~Cp:F%&1va\Rdzm.-^|bHCZ,~/Ӽ'޴W }zKZ-BPO#)~ G' Nx6e?A-}Z 0Rd@ 11Q' ݹ:DZ*pk9>wű o ^g~{CCKNh73-ly+(3_hsvvc­޳pp2mu4={LC+v(b{334ta:g\wtg~4jii,4Ĵl'wsw\] *A< ]%|o# J)LHό∬Gm'h1Di6g=frp]c;e%uM||{3\]y{&^e3 fX~m4bţw|ϐ.Cdn MPpnFPҿQfo|ht(q];1l2{l>(5*w-=uL,s9Gw8ҚET߇ߔ|'z^ԝB~$ED0=5iZ>C#Fr6K=ç[])߳^3eqh-y~;S޸b :u 7Ug5q%7M:B"!ZH"E&$wkrV_lJarq>GLE\foӟo=XOk-c!/Z`2(1d,Hk6ptTBۏPžpz"B߷_S">^P{2ZkT`XMNA+$P.j)zce'76wxJݯt/koZal'=xxw9O_+]=r1nL½&$ !7Si4L3uev:3+ި 3ѪEGeT5gJ*$99ѳ] @um46d. 3Cnrs>rxE48v>o?¢f =k=yل"O>?ǼمE=ζgB+YW4p%B$d}wplst{)vRΆZbBp*;N TP,DAWN\x@eaŠzfubCR I?J,J ը "($|Fz'LaMp39"s|G ஔ}zÍƵ"5xzo\nZ%f;ԙVޢ1Rƙ[h#5e2.Qc1$|mr@ v?A*&WZ7o^\pJ]:_~]뿮tx}F #5D0V ">Rt$0Vňkwq|\sS_gZDMxr˷i|'d.pF1w|\K Lڕ#sw$\3wTg݋7X̸r_ y.R\X՛qUHu'ǒwp/ʗ['t{), GpFk IKʁ\j}P*Cc4jBzX;ƒG)sWyp֟=8fvm3~z{J|VWwmxFc./hbQODθÌ5oѣ-e .)=V P}KyUņP~a_]EI Y-n *+J ڬ\4LJ..Zw}A)MqL>CCOp59]%Ft3[NCuuN74?ߺc4*3-%39o?`:ro>Pd϶Yڤ!m¢ s_<ٴUR;4=׹J).g|lv/G > àۃdzAG:{= ڥN R88T)RU \IHlH͒gB'8}؏9bMP} IDAT TbLDP䒣CW=!"t$t'WnPݥ.΄73kLc ]yğ<118܍mi΄+vjEck;<5 Ur}hC>UY9?&w5r?{wC@W/g+J``|꬏Y07``SZ=?ZQ?{>|'Å(l@mJWhEĄ,lsKoO6c\[$r]PB H.J6(J+`VYb?f@(]ˋ=a @)◫sJR+)!\dJPL2ୁ<qҭ݁V+P-t-ۓO=0g^3Ηu8}ksf٥={_0qÓ3yXMsP5#"ed:7/xbgL-*%gMACD ^_|ǚ#S5nڃLs[~.JJF~  ~ ȼhQ\0\p a})D+~߼W%,Q|E, 6Џ(Ardx>QZ" 6\%6H8S/eLM#kph[ϵF3=.hM?-δC-UǦ2Z//1|T[B}SfE 0(.ElD,-'gZ '?5q-da]j x s]8ׂU8P/,_+/WR_$^2 3$-0 a_5E`TvnDaBFfO.5^giGEOtp[e m?ġo_²Q1_G4U0K~gǷBƥ1g٣4%L,^y `HHTq).<8qn6 @J3XEf aPfWQ]r~[Lm}rK)+9yicL ^]X eYXY>w%crD:^)H9D[C >_B3c~X}42Y^#OL2At`("呇BAEA L>SC|Cg։#Zks ց<(!s(}A&Y7B wUKЩG37o/=^V u8q$rV!+ONv?KXM\);e``%"##JFy}T%(=N{2[;JKꤑo=Q0{O.uo__"~OɈϜpy jW@5굣y硘!FÂ^%͌/At>s $Nsv@yK@amE.$쐨.Y^DGz=ua;SߢDy | dCx!eG!|!_!PIEMği4F(:-H{>l nTycDڳ|2U C} Iℤs즦#2ـz͙[1\Ο1I<YP|ZI;D9Z Q{A@eatժZoэD̓d7|e(=j툞P@jXUĺ:V ~Ktk+$ k9HcP dy^Trg؜FAD,;S#2+t70lwż. vi>1r'ˑ:Wo巋sD3T@OQW*4 'u,u{7А2łIq:\U^1Tw ^VVPXD<7ё&˼ |/($jQ\ Q,{5=ٷ¢H92ӝ^f=rᝇO|Ge60peSt\oT%dkp24ʐIVL+t!GjOM7is\IR1Z}6xsw9?_Xh923]:C>j|ܶc Xx:vC֒2.%çN _X b+Pg4&~Ǒ7%P*$GYY>|ndw*OCfW(PbI8և~IF*BCYg1EuF(:ubsn,wxڪEp {u>s6jˬ*Ųޏ]=c=^gy6ůK|UUO]'r d}90#6\~Ş1ŷ74?x-1>^^؃:תd=18 KiJ,ժת6jP`%P88`YeADZ%ը",l+ZcE8X Y|f)Sקܿ#H|VΪc{]Ѓ)_,|h=/$|tgw=(~ևgH<1ořk/So4H%>=ؓ> ?/ȸY];* sU89.$^11 Mj74Z3f7n3<ɫf8.ȸ`5aa$ּhs>~|H%L?QcDs»XM|v6ϵYQzYP(pCŦtJA#=E#ʩOLО )E($j(T5yU9WP[F(ex$xѾܸ.T |҇ i*O9\bbeE4W)] #6vyn˻ֈosc;Ov1Xzުkyq2vQ?|Cˋw84{"k-ܠѾ@ I}ue1i5$"ӏ"OD桪l*ڹnl0LbJeP.c5`K 0RZ3t9n#j:z,zL~xk dyEU2/?m < |+O2u()ǽZq'C(|v Tz0jQB'=]WL{k8Vʽ@edYzFS- ,7s SQ"#zHjp{#滖 %v 1Q\aOx2pXoרvRu^S8(Q稒\%s "2aFl9)i@ʢ_kzS?.||X+ =}ǯf*1,csi+E,XE͹-s]LN9]h<"m_7`[I\[ta鿝 ,V_:6?tyw*ι2|_Es)I,P l;4"ȲuiDcr§*U/^E="XgT\f JU^ɺ]XPdWM\c 6$X[]9g8.A+Hk3ߢ:FrCŧ"8έevh kr!r-xYJ++T=|!<˰neqm.w {||{jqNAP60X"Ʉ>Z6UZWa9q,Wq[Y l9:($I2Tr+*aƷ✦"2E|}HIIH4ԣ:&4Wrpy!o;yڪD ·DE+*ևD>5 aD54i/\8Iz$I*Uz*rۙï_.J`idYF3hh>^}4%br{1cJ#aV!FIg]tj{7Uc*+9m>\);gX+~tp3DjVbZ  .tê#툰yk-ܿdMa=l:yI-3ƖnuCGp|V*$$D:*#d~(+JXki۾jO zZV?,<Qy1 ')%$*Ccn/O/ic,^%hgOpf@(*j]僧- n2>6,9M`aa(NNRtPT`X kۏ+BkY\XZG|ևmz.݅kP-ٟiI7[V?xX$,gMQȇ"Qcǎ?֚,=?KҹevmX}JY5+LʷZЉǷ9;٦3k@桬͈;j(79 cV 5 .ۧiZXc}VMX 諆=8^@o%%W1,..⬣U8zmi*/T-4Mɲ~O$g^pHrO:/1˺ƛ4M!=YUHε.s5I?ȶ[m=m:bK}=p ²[UOQv N쬳o"؜-v[7CCk-IEHZ=Il^Ve/4RVy{wl6IL1VE'MSz}1Eʊ; O:},s:65|WhƆf$"4M:sYk IDATv3݈O뜛uy ᤜgL)Imv7thv.r u4<*k`Dtd02`\pِ譠^ 9XsE 2B^R>vC F찳gjo<">&7_l (41lӡ޽l'qbgKsX^?|Ap1!Gd\3J;kJZo#ablFv 9uHq8(pj)j:Z+ 6[o3Vv(*weZ-4viD'_UQp#M$;v/yr&G) RGkunug%;ٖj#Q?πs<]*I9Jk1ׂ5{4!ы8dOo'r[ݷ_M=!*NBޠXXXۻWz"#2IYë$ڂ$exv o27_; Z$w~X$sE 9ŪU*$!r'nܸsvyoJ$$I) @f[,E k ~ K'dmTxr@V".Io7!;(@8Gc >.|?ӫ_ə-Q[qY+Y5g Y<Q`zyøT̞ɇϙQAl$e|y Cx0HMj--T9(ly] ExOPI0$$H_@i!2 '(rIzk |?!N>.zU?~U@h7n?}x Xg8o1M-٫s 0hzJ$2VzJ (hBW%&PE 2y^Unoo<^Ÿe7k,s F~s$ɢ6 cPAQ,Q !f)'49E{8՛ZDߪ<,yx9\}(FSS6y3S(p䲈 ,gQ>! I quue8XN^UVn೗uv >;Cf  IMٙ3z}Ճ 0i%[?6KtK_Dˎc}I@O/2ދyQ*Bt{t/9OF՛9>I[ u@jˣ2+(!a08ZYrMOߙjj+ן+*C$-\]$$* B'@zIH P眣Wz~yY ׍xggb #cʩ=$Ō'9RV |89>W&}#vt<]Λ:IK\zMWs͙(Bdy& :N`/Lܹǔž+J/T_ ? 58 `<ˌ߂q Ck9& Q!zJL3|y۴ φC|ݾ{a`bnfo 3NMD13t 4w&Z^8c7us..èjrZ/g H M2 Aze[tylgZnY18H>*F!)"^#zNq;OC"Εi I Gx3c r6"\1SL-fSZmL&D:$NyPݶ5?.B5ۆa,1E^`X ", qx L cFJ-q}^2%U'c rDp Y 0N b1&'oo(x$(5I`c!C%7x/0+t`1Qye!r?T_ҖQ%0d@7 t{'ÿ^Ȣ(zޜK>,WAH\jWޱhօa_& 98j0Iedk1I, 41\ R[L~|#.|4~<Ʒ^+%!1|#r%uuA }ժ&U cWf| \+5uN>p:AFc#r?h:Bmos\XVjVi%}@&(DDgx;"w^xg"Wc |`8|Ƥ3|vg{|`t|7z{'.+Vof!rU7ϓcA|qxdXP]35k%#ȶzXVV6Bz:PMC*_"wrH$?AaA!3$;( 9^ NKz)^FcT +Qsg;)DU0 ?k  ҧ_EMSF#5oBmvM`Knh"8l-Gګ΃e6)(p4!/П}_Ew)zzTU̫ àjNw}u A~1}i'f h g A8>c SUlD'0A+fЂ-QC$s}T f֒hm2clI=H= 2"FAqd-W럻a22g}Ȍ.H@k{NKS۶T2 "C"W-^vi(FbX4ڄn[e vM zHÀf<T;U Z_YZ[  `‰98>>U%vI`&N2cu@Lzݺ.@Uo.<0 M FDb۰n۰!Iv:lͣjPkWBdh^`+D` m ! X9y 7צK] sh"w_Rר?$ƻroY ۖ6h[{ zT[~+2*mdJn8!@-OY$0qss|nHk$@d=` Ǜ0.@Cډ44~Q@LV^:˩ϧ:z*w6꾞1vr:_mi< s rqsS: =WMaXm6t[ce VN Y^`|OK4lhTP-Mk]{qV+Ƽ<~W٧n+\I~t,.6iv:w ){8mKy/ F;o**&Y!O>tՂөl>U@yT~W=4;ח?*F뷁!Kz=յ*YM E!a~9>Sw}d*9FTgQ"d\h$?> t w3fzL'فu^ȳ Iٙ*DU(R|{a;ɶ|V,CR6>ND[C <٦ޘB =σG UrqC BƬyayT:BE;r8>Fka}0`#1Fd̩or?˲Xy^| /8W #I{.0*8z֡(Ľz~A hwaY,w"w%e9眗I00Y\TNvY$Ќʲ *f }P% H <+ф@;!֢Vy!h1_XcȲ ._ӑ]nqxސ}c`8!T'{C #`] {VRJS)@/TE־(dERҝX' h\A}*ALղ@Vy*g ]H뜑,K4)JTS!_jFD&K^ ;#]ؿCѦ7gq]6-wp8kB+ $D"^άpӍa"WRElik<9vm>Np8T/Pa* n2f^5=gU?g{, CٷJȝpKt"4MQ~͛9jׯ$]\Ea4IӴzz&Rx2X̯?zFf6Mvz4o7η5"e~GMh_kI~˿EM6yMT[=.<ۦަX[~`ݨmo tv,+~!>aYIb w2*{N`K.m{ip"P!FEVUo~9b7M^'G|?NyQWEHdGC\_=zԙ/qlwRL3[;h * Bd%:JEؖe_ fp`B/{;F 4  ĭx:1Žg0؈+0H0˿NϢܝU҆ccC>GwTy\]zLi!jK1]z`txř7o,ƕ/~ոzO.*[ ..Pk}փ_vP*ez$Yt в(0]Y %Y1^rā }^밞d+ -8vV`o"']&~,EH4Egn^ϊ"o.Hn?7GzlT [4 b'kYB% 8bx.J(J}\\ jJ2Vu][:VyњzAEQ, E% Ҕf҄ML[o65YP.\e|hASqR2j.߭cHui[H)IM{sa;Vҭ|'Yцv)NV88op*U*  ѡCzw]=? 4fG̕WIENDB`mailnag-1.2.1/data/icons/hicolor/48x48/000077500000000000000000000000001272232533200174065ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/48x48/apps/000077500000000000000000000000001272232533200203515ustar00rootroot00000000000000mailnag-1.2.1/data/icons/hicolor/48x48/apps/mailnag.png000066400000000000000000000054071272232533200224750ustar00rootroot00000000000000PNG  IHDR00WsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATh{\}?q3sgwv066k6(DE4"QHY%DPJ-M"jPP(ib9BM`YϾ<9c]Z`t4s= k-ΐ-h`Wq+GRԂ<睋oxE.|'ϮVld>:tgßlG9Z$|kFe9lxT<w{{Fwh}a㢜w%M l &ƚL 5mYy_T?Wِ8C#cτ/a% = !M^,(!nꥏ=xuBBkImgovW5k6&šѢ"0R -$ٹ^9~uw%j>B+Wi/ж'}+Cܦ0 4M:~OaX!L X9X 1vE1.;>, (޹s~ eَeT^:.TAv>~y @Q&vfq"AvARz  SmiPJ v ސ@&Lx&]EW>)~qj;?W#_$kk_A ޯ +,ZdBf Ɔ ]N49zz̬\hks-Bla02} T<pӝlK2W!LBm0$Z*\E`po#&Iָck_XmW\^5 xȲ v zaȳ/R5X]/lH YN+pYɀXkT& ߰ԉ:/[Ztx0^<}Ώ,`Iin8?'umH;[@ݍ( QԜ (DOSsz*$7<>_ѭ,˰^USi%{*<zt>[/יmgK秚)6O.kK^<܅gaO#9c8IM/9O駗-jrcqQamϞ=L~F=gG="QjYNE=%win8vfĤu'’w @P*x (\sDn/ITrQsv#6WpG1my -$ǬSoRpfDQ?.gS+ XkK [)I&-vJorTd/#-|*AquopRoBrր~tsMRk~Fnբ+-sW8iUJ8M"zCCCT'H ('gZ)N\lP+];R56}vh;|Нhܵ Hkמݨ8AX_m\i8j5X~pzg_&x[Ir.a1Rc&6l +ؔ4RAd+$#㺮%!TU`Ű"=E]vn:ɗ&0ᖚw06T`Ha[T3'[LǕ3knz<,j|@ev kwX<٫F:S .hVf)R~>o-˹*/>/[w22QIpRȌZ݆u<փRZFDU?C=R(5tgco0Rh sc0g{BEq][ nGRG׵Jl6?hxe99QVlVUw„~ic{[%%WeX9f`.|硔FkYn%p#KRjSCn+OE+1$+_*Ӓ+@3*APB\=#D׵Flъ !$JJrn#]ZMˊ,$ɒ7 \Xk MADQ-u@V.ZՂjE!(hr 0׵ 7-|%R8Q*tNSęEp@IؽI2:MZk\]vOd~!rg#)Q'{}1 c'`Oc=>{}1 c'`޵yE~$Bu3|7 2_QD?Bx?@O8΀`~rlw}v(c?0+yL09!sro#lv\{13ӆbl\jGVכzl7c? .M/YaY|BTC7:>Y3'~eKP.VIot  iOW:;B{'Qs 7|E@\j0@Y}WVe87>ZomsBTDgjSO4yn>S΃ ~!g!;S\^ Xp9i;z- Bϟo>O}6¿!K7j<793+nJ: pRc{̰+qNXk!Q@c`apcy\f sx ӟIXy`z` YG9@!?}mPڡYѣ Z5v&(!nj=̵7rL3<| umK9_v <Bv>@o"334G+x)aPy2xz]K9p$& |HGO5xs!ͅlh,ϟ^Y; 8+}m9GYDMq0] A]pyE)d@4=7CKmH΀-qD#lXŠ͌Xk8rC90-/ͅois[lt?|溔dŔ%ybrS~ՎG&78Ǖ^Ө֟v;SxW\7݋;/ _?fbT\^[z:\|tGEptsߝSnr:uHU0lUuȯ}߀kP\1נCoP|zuA$e-GgK:F!#AinD3d$[M7.xX‘WJJ~h̑s}u7m[/g-9Y,q*Q>ѤK ˫XضQH;ȡ*w٥W'%OBK^Bo7d3#^=c$RJ pNP1H[d>+[K6zh4nrr,B>˼Z+ >|2e2<Ԃ' =:xNxR K7JǷ/tz[H >|2XKeLBS陷(EQ@) b䞝 s P8gqe+s%kbc~֤I@( I2 KjlX|>t,!ZzŽ~_/9s=27\^)k+|p& 9KxGVn[ r@ȓbKR^-4I Gܫ٤ɸnsSkJq {o>‡]{;hEHt hZt#:xǕI'+OO8T|)[l>z[=|DBגPZw.9؊M|C=[ČkAifh4-w@C"!H;Z5Ěg݁\8?7_ƶ~N PUSR"0"4:s\XRIQ0\􉘇Wgͩɀ-3uvz啂':1|Y1ͳ'"sv8fo+=K(DTz$+(ڤb !qB"8!|r Z@;/n~K-V& v޿Ȃ8HOuVłLT!ւHᵜW2ڃ-r3] !LǎDđX^xR7~6 lµ|CAu3{ȵq#&(S4O?3fp`P R 5Zk Gڝr9ƒĸ͓׺%Oju6<8p0|Bt|O8ؐeYC0R',-BH, mq01 ?C(cfo#:Q%O=˯q3n#$Rh u@EazeNgMs)s|t%>~@S:?K-Zbj<~$$BsBƫ󹷌Xc '5@L3FDu >B$ =Oü*/R:Ҁ_4>ѧK22.M$2L A@qm1U/ ]ؽ {5/silBBC 3'"U.-|Bbߎ/RRZPpXwh VPqDz03zI):S8"T`[AC g>$UI:QB˜8LΆ=>p8'EC3 >P.|Jf=J yXD*B￝fG95`f,`Jî5(QFh%ZpBAD#FKbKܱA,R \䢘fL7+sg|#-o$-'7>W;ELXbkj!}gwL5s#I7Kh%t0 Ð@k䧀}>5JkpnXWl;ڄJ)R(%Q牸{:\S,<\X6O<~4$ $9&b/?;srWJ9ђ-\{ 5#9xo´Xblk$ i1A G3:^)pر.=TG /DJAe$+\MBpC/`uo8w}'9PUΑpX0ah1ah4Hdvk|װZʲ%XJ(DJT$av., e\*-ՌhpVD8ndo8XR9Rp"`,?یICh=> j >r4c :բ6sHctU4pǦmٓ <m '&CƳEZIDQDE/(]kfs^GiJY57]%+6p:LԦjU q zN^_kkoym>_LAI)>8,9:b DQDkb}Ds+$씨[&9G#bz=8^he`0Z8aRt="tr3)1V,KϽ&H_W PIOFj7h5db(9Zx[z1Lez#arMl9 CQbm|b%~$Zz4ĀAˆpzY^ 7zPTȉ`41߳ CV4)EQ=^ode}ɻC-0s_t(鉨6~QkJQyT)?S?|K#I &m)gWb.-,Xs!w{&BczVqBPo M{vfrC uQieA,ˆy@JE&C;UNƆNk8:@7y^ dB`@S 8]4,g> 9fS4B#"N&]F$iDqT$bra! %`$욀,QJ[a ;ܷ , <1 дZ-p=( jA{Uu*.fL g#f3Q "dCXf2UaZ-0sR5nŝ$aXkQJMwk$IRWE ql垵 k%%AJfKN[qu@/Xkͣ3)'.FLPIJ~)u5%%,ok;E-J݌Z{ҼL8@վf(sC(V(Kiʪ3⾸`H%)0g dEJNg 7Pe70jj:ڸ6YQfjxI)98h )(e&55ZhBITߩa)i6[t:J6,Ջ@-02X/1Q%nlchsk@unU3?ROOU$yEԨ}V‚Z)n?3*7_^]%a{YI5Y?ZcQ$Y'2G6"EZ"UYvHYu-Z#uRfA']b*7Y:ƬfTkWFhqHƯU#ʸV:]U+Jr*jJ!Y CW5vOUX(B*bVIYsv$'jS)BIse8eCDq~6 G۽%`)eZj7c! C _VcIdMy/Z}a%d}nof! C] AӇZM8_p`b_{y6};u"DJ[JGYбYjtd!uW,a k( vzJݎh4X#wCf TRJ9ca *F'=w#PZ-_ g'{6df:"\pnazuG;w FcsuϳFaLW ?^;,MSz>֮;ګ Tj\ ]!h6 |f)DYF9xY9 >⾛EEۼ 2]ZquE.ٍu%ZOJJA0vzkicj4<'˲6# av_䛟wiSR./?6yAI QNO^n%WzTnI[F~ PcGwhnSn9듌~Wc+ClN}Ѷٶ;m͂ :>b/  B6F>דr6q 9[w;myS ;V[M5!{qVZB8e9x ʏ c'`Oc=>{}1 c'`Oc=;R_IENDB`mailnag-1.2.1/data/mailnag-config.1000066400000000000000000000011201272232533200167640ustar00rootroot00000000000000.TH MAILNAG-CONFIG "1" "Mar 2016" "Mailnag 1.2.1" .SH NAME mailnag-config \- Mailnag setup utility .SH SYNOPSIS \fBmailnag-config\fP .SH DESCRIPTION \fBmailnag-config\fP is a utility that makes it easy to setup Mailnag initially. This only needs to be run once; after running it and closing the configuration window, Mailnag will be started automatically. .SH SEE ALSO .PP \fBmailnag\fP(1) .SH AUTHOR \fBmailnag\fP was written by Patrick Ulbrich . .PP This manual page was written by Vincent Cheng , for the Debian project (and may be used by others). mailnag-1.2.1/data/mailnag-config.desktop000066400000000000000000000006051272232533200203040ustar00rootroot00000000000000 [Desktop Entry] Name=Mailnag Configuration Name[de]=Mailnag-Konfiguration Comment=An extensible mail notification daemon Comment[de]=Ein erweiterbarer Mail-Benachrichtigungs-Dämon Keywords=mail;notify;notification;config;settings;preferences; Exec=/usr/bin/mailnag-config Icon=mailnag Terminal=false Type=Application Categories=Network;Email; StartupNotify=false X-AppStream-Ignore=true mailnag-1.2.1/data/mailnag.1000066400000000000000000000014041272232533200155260ustar00rootroot00000000000000.TH MAILNAG "1" "Mar 2016" "Mailnag 1.2.1" .SH NAME mailnag \- an extensible mail notification daemon .SH SYNOPSIS \fBmailnag\fP .SH DESCRIPTION \fBmailnag\fP is a daemon program that checks POP3 and IMAP servers for new mail. On mail arrival it performs various actions provided by plugins. It comes with a set of desktop-independent default plugins for visual/sound notifications, script execution etc. and can be extended with additional plugins easily. .PP Note that you must first run \fBmailnag-config\fR to setup Mailnag. .SH SEE ALSO .PP \fBmailnag-config\fP(1) .SH AUTHOR \fBmailnag\fP was written by Patrick Ulbrich . .PP This manual page was written by Vincent Cheng , for the Debian project (and may be used by others). mailnag-1.2.1/data/mailnag.appdata.xml000066400000000000000000000025551272232533200176070ustar00rootroot00000000000000 mailnag-config.desktop CC0-1.0 GPL-2.0+ mailnag Mailnag An extensible mail notification daemon Ein erweiterbarer Benachrichtigungsdienst Mailnag is a daemon program that checks POP3 and IMAP servers for new mail. On mail arrival it performs various actions provided by plugins. Mailnag comes with a set of desktop-independent default plugins for visual/sound notifications, script execution etc. and can be extended with additional plugins easily. https://raw.githubusercontent.com/pulb/mailnag-design/master/Screenshots/mailnag-config.png https://raw.githubusercontent.com/pulb/mailnag-design/master/Screenshots/mailnag-gnome-shell2.png https://raw.githubusercontent.com/pulb/mailnag-design/master/Screenshots/mailnag-gnome-shell.png https://www.github.com/pulb/mailnag/ zulu99@gmx.net mailnag-1.2.1/data/mailnag.desktop000066400000000000000000000004651272232533200170450ustar00rootroot00000000000000 [Desktop Entry] Name=Mailnag Comment=An extensible mail notification daemon Comment[de]=Ein erweiterbarer Mail-Benachrichtigungs-Dämon Keywords=mail;notify;notification; Exec=/usr/bin/mailnag --quiet Icon=mailnag Type=Application NoDisplay=true X-GNOME-Autostart-enabled=true X-GNOME-UsesNotifications=true mailnag-1.2.1/data/mailnag.ogg000066400000000000000000000356361272232533200161600ustar00rootroot00000000000000OggSaSvorbisOggSa5=vorbis-Xiph.Org libVorbis I 20101101 (Schaufenugget)vorbis%BCV@$s*FsBPBkBL2L[%s!B[(АU@AxA!%=X'=!9xiA!B!B!E9h'A08 8E9X'A B9!$5HP9,(05(0ԃ BI5gAxiA!$AHAFAX9A*9 4d((  @Qqɑɱ  YHHH$Y%Y%Y扪,˲,˲,2 HPQ Eq Yd8Xh爎4CSR,1\wD3$ R1s9R9sBT1ƜsB!1sB!RJƜsB!RsB!J)sB!B)B!J(B!BB!RB(!R!B)%R !RBRJ)BRJ)J %R))J!RJJ)TJ J)%RJ!J)8A'Ua BCVdR)-E"KFsPZr RͩR $1T2B BuL)-BrKsA3stG DfDBpxP S@bB.TX\]\@.!!A,pox N)*u \adhlptx||$%@DD4s !"#$ OggSBa':5 |{Hwt0Z!r<%‰{\9wMԪUn!03=1[iUA_uw+h7q)ن;ޮ9't{[āCWꡌ o>`?EJ("$2 a!AP[ܠIR5]TM r%C*]b}ݫ ꃇ eg 1T?=8䤸Vz}{0h]ʊ5XJ8\ݢ;_ -8Fe<@֙_Gdyf_b8w[u95 Z ցV` C( z@?W`u Ֆn$rJ&(ޤ)W'ASQL @Gnb?tp"^ @5>]- !8XJ 6Z10 Q*1Ph6w{~HX/^M͟w#- t`J&dk" `CF " FB3M6 =qY_i2#0ՈnI- 8l5o v>yl$50q (Tj ld@nKLif5GqQSvԻ{3j`Wb$`E<$6!@h,U } ؗ6H86I H1n'u i @"N_58 d4;T cŽU. {;=R,&0b Q,"Z,:T 8."uW`}SBxNnv[{]$j/W?;L;m XnT*I!BcAfPdP T[khqk1z/݁1~TAM~NK Ndc; WH*#mtdHµB + \ p@~ܥ;xQ~C)zu#k3;呭DMw / X&oZa&p( (6+9g vP ivl֑M]SՌ[a b<߽^>o8 0Ar{QCR, 2CَHɓJ3)ŇD 3[KPE:iravmcnx5zw@  y!?> ^c P-dU 4 >i.]x6 5=˾P|ϴ]M|W~f;F%q($ @'*&Й c  \Р%8b''j ^xɞ%'_ǣG\|5җ+(V z&RҶg+iQaIC!t Y,n~BkyYyWyҏ4I^~C6%5$Pb0_>zU?>OY1^c4?=2D`{"``g2\c`~y^wú(0ſ t|×@fGD!@r/0XH240dI.]q)]kx@>3yUQ;K߾o0xU@S%XXW9  ڤϿ E˛zRд+=p#Boe! @ V?@U}c=AU;};G'&:}1D$ȿB|0q d5 3l s(%8n*yg]zQ 5@xp-vC @>@x#"8 T@ K19g:[omJ$S*e%M I.|X~fl*ce`cKX*@`@L2qhd(.Ĭ#}k7j8= w;KԆxy Xߝ`vT*3!*Hu9 -Au<n ?x0kHն cyTҊPDP\tn ml;0K0I\|[ah$lhT-(d޷-bϣDɻcDMq4Hj1~0'.~ UUU@:sz*SғKg{yQ"ҬX!phjPl @L.й8Q nc͝Yd]P$g|f|,N?Lݠ g%}=i^"p1k?d04X!sqg^Ӛss>gM2O v-KJUğ"(v1Y "- /XUUEX+<ܷ[گ7'NB(:x֏ e#;\ (:ZPr)iOZɵN6YnfA&)C_0שռI74S([Ff k+sǐQfn/| /T9TUC|=:%r@3ժF &(>g"TI?kv"ժ((()wbԘLoe0;YOrUUUD^ϼښfX۷o֯]=qu7LJ+J!*"0y*4?1մgn@)W! dC=&w_8:{ E66/X!ߩB`1g*%P\tXw yJ1ӆDm4p^w2mBmA!;d=Q Q#䮝{xQ{0K*m0TUІfѻ*x<#xݏ;<8vWrpgpaG*Р@p Xk8>9$tf' S _v}]gwt7#0LΉhzbѴyn7кji'Q);lV<>ד#2_b4cTn#Gd<ߖˤJL:;-B;Q}]rU\VZ* ҝPdtԈ'V/~P;Р3b*+:#>s9S7 y2g$B@Hl2rjmgY[>(A_tS?7b{i|S'ݖǹz_^v}?ݪ.͔ ٻcY=[&V070ic.\`oCV =sGKSc2x{ :iq =T@Γ@(3S@J6ε88؁ᝇ (6$T,C7 )|Kj6`ݗE{04Dzbq.X%F{^9wcWV:W>IR_&ߗIiaפoR|֏U7ȧqwd4GxG%ïMR-jg+dKU<@aUUUu@{bguBBP2\>u>npb=)9xn7C=k}hE0JӎxEovsvL߁f;S`;,U.`'TA|?g:mp)pi&# *ހ.JVNRJBwWv;J_8ײ4!Dy65%/x^7EjE97TUe3D-p̎\F<-De690 [7@g&3:s;~Ԕۜf}uOU~iQ;wޡζ,8 qm_~J!Ӎ8,P5r(f{RSi2՞H{qx3j ^gf$WyfN_\\;ZְI.,6+P#%8+_/FfŪx <2 xPTwTш!D0aL9_T%K?_T9 g;_ɜfhjoy,s93gVg=\ݐ8bwvCN_ߘ}ѻru lu.dRc UGw9E*4"s2U=s@W vD|f"jîeW)('!EnjDVy+oeFXUUVB[\`^7"4|Xf3+`:nB!SfgSp(tTT>f쪓avܖrbWEl57CUHҴµE^8̜Ӭשּׂ`UP贌܅RfU#MSPn*5Ғ @CF 6$;m%W5ўMHg +WlDbr B"N%HW df*JNBeʷ4߾M@u}RI؊BAR؜Saާ1g}8PO>urٰ740`Va:]uWjd}ዳkOBBԇ̇E# jվ`2K\+[OG돼7OFsQqcwڌ7}Z~=$ROt-ǠHvD{H5/@i7Pn#C9@Ѓ[B@Gv4/vHW!Kq-4 ;lpa> ?/6>}'I ͆i2 s&mۿ<-3B*gjpzT^+N/. cYF^Iw߲ )}"!NbM]6 ;1r֢}kҶUo6sm#& ~8)Fr#?jJ%"jjjLV:eLBBZ;8Y%/[(`7`NEV{3ѝu8uN+9ƘW{~:{xM̥״9E~9|l=fpf(4 ٰϿd~ir]k])so'vkEG1u~YgibW./\;>B4l!cOBQ4P3ZvHrX<%JF1XAFK,PUU*B:gJnN @ Of_LhY}N@=U{;;=iN1|+{_c;ޘaz=3k%Thw4?*c &bV&^uk@ƈ ='l99sI;XaθjBsBM빈35j!Ŝ`ѢU钦 7IH1{u~e媪{eM@n=G Ui$)o4II>\}N37}}6l;7':`ûްg69497ro`6$AOOYuo%8HXݘI̺vLz芡YuB[GľY(`PI֣!nSF7Uys @M m)j&_ˉO³"LDq^\篡$#SJ(Ne3>|gJXM`e:7lm~mu|?yN%'k3V_0IS90u}7C}ӟ:g}k: \;ȹ[dƄP+&v+jH0ͯ ly2<\9i9p,~'<ծsWvpo@CR-[ԭowI&΅hgON;m33j_B@?lo95VUp޵3IrOHQ2F5 ]$!D|fCHk:jH\UU ~_ S2J8Z"Sۺ]Mմ.:6a;:5C=?$~>_󽈟S3͟s|uFؾIаvq|@ogf]@ͅGruP+@>/ #QOӊ:P#מYN.PhGa;3?z71Q d]jP͔.Ik9]P$ &=;RKT<̝?Lvg5kl ~iyM!:k#-%, @H>H]f"o˝^MʡWC eMHo8yv#{Nb:#qG̚5>ܡ0CAr%Ar5GnLNl:Z:.mߋ_9+[՚56so1S+91{?EN3sg0 3-B8AX2$Ó_ք>>)$~E?#$NvAκ* HwHY:'zn1+RI'k+\ubT|B_~ >d MзA8a`# v5)v<|Wo rˢˆFo[B׸ ~=#} Lȅ9q6'Z^"9܋65+ ??'B(!>Kfc]&NZ0+8LHɗ"Q3J$OĐ)BgokCMew--ToG̻v"D3X] ˡ6e .ҕuN|vqFk8# vLkR?gΠ>G[o9n|߯!ϭhMYe8kZXBB.H母QF 5/d56A䙧pBB MqəCOh>&K vG2֜~:8'N໰iHV,wfh? Q-IP7a/Zr :;,ǀ?@VȬ3?+-D:!J:F,<ԢIo*HP3ƠM *"Dk(gVnOnOG,/Án<\_Ik5h%'FǸBK@xX_[XOFwCk5ּz;&5ElϑK{8qm%J.rXk1Zc/A^MO!oKN*#.}=<2k!Ap̼?ߦ&]&YtLj͸71ۮvQ-pzǹ-BY3L(5F`>_ p)beWw\|Ub#MZ;!K 2-pD~3shMȖ>o}-#Kem1f`oo@83[R:fA3N;!d6R yw> ~vyxu5%d.l[O6a|b8kI69} ܕ=8{+WdSF;g30C3!@^ğ T]dfHT3ZtN3H_: t6 5|*߻'1fV tB(:ʝH"TR@"!-@ #V1-XInmF{eX ! ݖ&gz|7e1# ]7 @F..s|BT.lmk,Z[P"B _F-ut z0HXY)q6wZKeݧ:E"7H86|/ߎѕh6*..\My#M=++)߽8$ڤ#cƘ i3$d3B";ߺ0$ջk CJaf6y-X2um[TY Հ"j#ZOU8zn-8bHNuÁE߄Zd[NSߢuGV<.; -Go<ᐍC:çۼp#k1fpVʝB59y;w~6K)O[vy2|~3b}nO ifP}5 ,@^~1ҕvJPƟ'iExg OaM12g?b釿Dzqۭ(̑Rxs] 7Y~V\(h8ۇAwRTh/i`F c4$OH eF9q ifɴ@kRߗBik 4ijRO Vn?N&Bi{Pwlqf!.>xߩI$ x#M|iIӔ,&H} /#L;`iSb"kcJĆ:w2WdKHO~qq<k7c Yv.ҿ˷VI23/[xP;zJU3VO+DUh[j9$Zݞ'.u̻1RJTxbr/(S("Ic}%1T%ϾD 5 O-^CvRՌlrjn{WSq~HKV}'c=e-h+@hXZ;LsGMbfֈXR?Meܞc\a&2.XkWj4RJwZk2LgDJ1IުUu+=wvn-X _ye]ԩ C]}Z̄ӯlk-Z;_jϯ+hEg_/}$BA8hȹ,PyWEWkXk` ԁ =]#Iӌ4H,͈8NH$N,S'Y-;J# z|B'p'1߽w-_S-6./g|e+!xo:4=SB,;sme#*sJQhfVZđY;2lzǗou\MbW -E~"Fk]j4K4MSG4M D9* MŇo2׼^ʀ[|whMl>KC{V@KZ5GVZkYjxv~BVhaĐwyO$ }0gXr&nD9Hr[bu3-r- #[;\h]ŞZJ)mruF2$!MSQLhn5O2_xwXJ\I PǛ e|ܐx>cn4[V:=s-#q4# "4 ZrBƙMEbkDmRWb4gZҏVzg_<[/5%WnS ΣveNi㲉Ys= q i1scB۝[C=ޤf_sݑB[g;TkH>sCneaJ)t,-h%z bt 9< y&-VF.#эe;rc0d#E}7yڕ[a8жJbP9D$ NYJ$ a&$! bDOYm[<{mdN0 ݦXHmZ y>ـ¿+_P}LV9Y&F"&rij̓o~~U esLB.%i 2I=MV\V'HObر=OjrfqD[v>]t&0x`O(L_|v0c4KʋD`DF_do\\" ?/=)9.,e|)\%Gs&Qph(rn{*$47(i8IPR ! z'\aFt"KI?Xs..|&Q3ϟribOejUw'f:Tnvy%H_Yܹ!etsRBpbΧ\L,:(yA#RH,VZ̐Vc8bxX KYYwBe HQ"@IJIT>T*cE*Sq?x(2`'3V"\ y# b@ohk ߼8,#0*lÓG񐇒 v(Vňg[JhC[|xfBE g<|_>)S٢t.geIXuǦ:;Ah`lq?+[4EJ)X`x55@ %kݑWS~LCH\nϟTYAQLưF8Q|txR*ɣ#hF_/6uh݉|C҈4<spxBIGd!B82k+9bk1c.'7Lniڭ0 nb euFf$I4yTp8$Wg%aG@"x#!8:U }F/ !&J+ Yi)<cy gKe\N 6>ϣ/89a FOO(ʓc]뱕uox'2Z@};qb9p: euťպ8ɥJ4A^a? gflI[Wu3?el%R|N{|!w"[o&Ul𙟬qm%/ܮX# u}lVQJh%}?$~mVBV vrX,J]4cx @)/w:HF@&>%Z 4<;YZWU[gvY./eżdc^؅'%;_7dVrgug ;7IDAT?0ҼN!:M [D% ِ˗^S/:$37qԽf9>v 9`<- T.Ja$RZd.dz^rN:5$NX܅}pd1l'H! t;x˱ei Vźd3/I-_dXZN(7<;EW`~Ls8p|6XCVi5a"W٧YJ~%HB뢮إH\^ Wnm\iE4A"$V&c/H';Ft;ؠ@\dƬv<ő=)p?eTh<Ȥ~= .Z$|s?cØ΋CA'%k*+?fpghʞh0.l43gN7 pCd[p[wY>wn 7(.D*ɏ!:\|f3 g\%v_:ϛ+؇/g<^">kWpDޗ#RnV.+n0Ar1ӲlZs/&|U(r~13/Y͔ 1f-g@"/2.nƞ}W.*w<'0Z`0 IRFqWNXe<+Zg.D)|'RL;8\.dm [9pm7,Zo7|q/i#9|\54L]Hz~ޑny o#f;yq#/"DH*p6wElA'}#twVJΔ' o6z2orYP`a YɴFH<4%Fɬ5xء,x3=:>\Z]o 5ޕstWj++O8diR90$; @fBQK\))s\pQk=6Ma"}!t'}Z&F|څ%ILZif0V6.Zm"W ( :iByzGxWWZSz)~/~:Iw_= -Uݥl+N/X2iMV8HF#,pQ+,cE8a!kN(mp4 GQ`0 X,{̔yyc@eDQ[n?~4XB|Zg$7u{=RmMZӯyv1+k0dA BhjV(%tkztf:DØ,KP~*:Ћ Z_> ~M  ~Ng =tkY]]=WJjH(<)qLp!ÇR4-~4d%##t7l-TRx}݂7nB꟧贻 N)epoV~ ?8m-|SL~Ø6n\Xˑ2WuTx`HEWvBR [wIHWdUL04#k3Fg b8+R9}4c9I2W6-Q 7M;ڨGɒ:ﺴ0BW4ye0$]oekEB!$)DkKIAcxKQ97Xq~%|B]/M_m曂x4̵<EQdJUs9M/oJY2[&BiZawB( v*aDfaa@wH\^kJٲ|+]+%68~eut8)Y]᧟MG4=v{r2Rݢy1h)ebβThNr1"rSd69<2(~SrN'Bؗ,BiV Z+tv[3?ÅxgUNk\Q3[.CHW*OrRKwͱn{=N8T\E7Ngju47;K:Y0 6r&[ϖYo7*;(%sʋxnFZި\g+[[p {j(j/ Q[-t {&fyβ\&^bO5t{f8;-cdNڝa2FIB$ -ڱ'SU1G̸ MvjBћqȼ$B X(Hs(Bra32 s2[ i233H!iZ,&1Z4Oө3'e4^cZOr޹I6ȭ54'!(Xʌ tk!(W*@X][%m-Mo-Yup;i+ϣ ƺp0k?̨:&eGAj MVU?@d7w*OGtfHYN1wz3Bq3(Zߋ̥>%0;+{=yw5 NVIE/$@ن`pyjZӚWb"V~[i0F]BJIK[P?EB=o`[^n1ύ֔2&O7Du&z'u*ˎB{W+Ӫ벶@i$vVz|7 AnWJYְoc.:WXo8zZOctq2hZL$](<);}fa (oΘxЬ4S~X^^)KeQm霩V+gBi:MXw<=V9/¼l4Nzɓb}i0Y6wx.ZH֌*=ߧ7˧?ykvy+Zp7 ǮhtRTQg/-]z 62pRPJFEv_bvvCVeht&Iaa6UmKXCzc#lX{ZkFY=8)c{v.ʲlbzT& TC)[p/]r(Bhƅ[QBZSUyո:6Q~0]N}>ybaaޅm&?n~b: /j6$iZ_N-$(r ylbj?ȳg|۩;y$ShAQ]؃4_1h!5j0j&;.?vL0a\N7QchrF86: 09N|\֠]zGיeQ*o5klڲ2Ib7s` nT;Ѓz=/jֺδi2F7y-,g[ӟ!G͡HH$Lқ،4H-y}?b[=ը1k }˟{w,H f$N-p\ݵ{n~4)eޔڜEֹX 6| yoDhSY{&3Nb ucxPHm>LD*DZ݋m;.s#qK]mBbb vJ{]@]Ve7{ȸl=iH1^b;E1l{Slw;vL{ܶe.w[ꪥ|=lU&lՂnnt}ًϿnMaSajyʙ곯7J$wဝe `c=Uz}ս"\Fdؒ ~ͻi6ъo;}Ny^ 4KS{kIx]ϷJf&ajI^y/Z7~ֱ2 Ioo{ZnaLlvHr%{S/AُJZ~c'D.+kwAö $л 2XB?x.42jBWKW ]c_&t}5jBW ]c_&t}5jBW ]c_&t}5jBW ]c_&t}5jBW ]c_&t}5jBW ]c_&t}5jBW ]c_&t}3IENDB`mailnag-1.2.1/gen_locales000077500000000000000000000010201272232533200153160ustar00rootroot00000000000000#/bin/bash # # script to generate mailnag locales # APPNAME=mailnag PO_DIR=po LOCALE_DIR=locale # /usr/share/locale MKDIR_ERR=1 MSGFMT_ERR=2 # check if a custom locale dir # was passed as a commandline arg if [ $# -gt 0 ]; then LOCALE_DIR=$1 fi for f in `ls $PO_DIR/*.po`; do LANG=`basename ${f%".po"}` DEST_DIR=$LOCALE_DIR/$LANG/LC_MESSAGES if [ ! -d $DEST_DIR ]; then mkdir -p $DEST_DIR || exit $MKDIR_ERR fi echo "creating $DEST_DIR/$APPNAME.mo" msgfmt -o $DEST_DIR/$APPNAME.mo $f || exit $MSGFMT_ERR done mailnag-1.2.1/gen_po_template000077500000000000000000000010731272232533200162150ustar00rootroot00000000000000#!/bin/bash # extracts strings from *.py and .ui files and # generates a gettext .pot template. glade_dir=./data python_dir=./Mailnag pot_file=./po/mailnag.pot if [ ! -d ./po ]; then mkdir ./po fi if [ -f $pot_file ]; then rm $pot_file fi # generate string headers of all glade files for f in $glade_dir/*.ui ; do intltool-extract --type=gettext/glade $f done # write template files pyfiles=`find $python_dir -iname "*.py" -printf "%p "` xgettext $pyfiles $glade_dir/*.h --keyword=_ --keyword=N_ --from-code=UTF-8 --output=$pot_file # clean up rm $glade_dir/*.h mailnag-1.2.1/mailnag000077500000000000000000000105111272232533200144600ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # mailnag # # Copyright 2011 - 2016 Patrick Ulbrich # Copyright 2011 Leighton Earl # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('GLib', '2.0') from gi.repository import GObject, GLib from dbus.mainloop.glib import DBusGMainLoop import threading import argparse import logging import logging.handlers import os import signal from Mailnag.common.utils import fix_cwd fix_cwd() from Mailnag.common.config import cfg_exists from Mailnag.common.dist_cfg import APP_VERSION from Mailnag.common.utils import set_procname, shutdown_existing_instance from Mailnag.common.subproc import terminate_subprocesses from Mailnag.common.exceptions import InvalidOperationException from Mailnag.daemon.mailnagdaemon import MailnagDaemon PROGNAME = 'mailnag' LOG_LEVEL = logging.DEBUG LOG_FORMAT = '%(levelname)s (%(asctime)s): %(message)s' LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' def cleanup(daemon): event = threading.Event() def thread(): if daemon != None: daemon.dispose() terminate_subprocesses(timeout = 3.0) event.set() threading.Thread(target = thread).start() event.wait(10.0) if not event.is_set(): logging.warning('Cleanup takes too long. Enforcing termination.') os._exit(os.EX_SOFTWARE) if threading.active_count() > 1: logging.warning('There are still active threads. Enforcing termination.') os._exit(os.EX_SOFTWARE) def get_args(): parser = argparse.ArgumentParser(prog=PROGNAME) parser.add_argument('-q', '--quiet', action = 'store_true', help = "don't print log messages to stdout") parser.add_argument('-v', '--version', action = 'version', version = '%s %s' % (PROGNAME, APP_VERSION)) return parser.parse_args() def init_logging(enable_stdout = True): logging.basicConfig( format = LOG_FORMAT, datefmt = LOG_DATE_FORMAT, level = LOG_LEVEL) logger = logging.getLogger('') syslog_handler = logging.handlers.SysLogHandler(address='/dev/log') syslog_handler.setLevel(LOG_LEVEL) syslog_handler.setFormatter(logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)) stdout_handler = logger.handlers[0] logger.addHandler(syslog_handler) if not enable_stdout: logger.removeHandler(stdout_handler) def sigterm_handler(mainloop): if mainloop != None: mainloop.quit() def main(): mainloop = GLib.MainLoop() daemon = None set_procname(PROGNAME) GObject.threads_init() DBusGMainLoop(set_as_default = True) GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGTERM, sigterm_handler, mainloop) # Get commandline arguments args = get_args() # Shut down an (possibly) already running Mailnag daemon # (must be called before instantiation of the DBUSService). shutdown_existing_instance() # Note: don't start logging before an existing Mailnag # instance has been shut down completely (will corrupt logfile). init_logging(not args.quiet) try: if not cfg_exists(): logging.critical( "Cannot find configuration file. " + \ "Please run mailnag-config first.") exit(1) def fatal_error_hdlr(ex): # Note: don't raise an exception # (e.g InvalidOperationException) # in the error handler. mainloop.quit() def shutdown_request_hdlr(): if not mainloop.is_running(): raise InvalidOperationException( "Mainloop is not running") mainloop.quit() daemon = MailnagDaemon( fatal_error_hdlr, shutdown_request_hdlr) daemon.init() # start mainloop for DBus communication mainloop.run() except KeyboardInterrupt: pass # ctrl+c pressed finally: logging.info('Shutting down...') cleanup(daemon) if __name__ == '__main__': main() mailnag-1.2.1/mailnag-config000077500000000000000000000033351272232533200157310ustar00rootroot00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # mailnag-config # # Copyright 2011 - 2016 Patrick Ulbrich # Copyright 2011 Ralf Hersel # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import gi gi.require_version('Gtk', '3.0') import os import subprocess from gi.repository import Gtk from dbus.mainloop.glib import DBusGMainLoop from Mailnag.common.utils import fix_cwd fix_cwd() from Mailnag.common.utils import set_procname, shutdown_existing_instance from Mailnag.common.dist_cfg import BIN_DIR from Mailnag.configuration.configwindow import ConfigWindow def main(): set_procname("mailnag-config") confwin = ConfigWindow() Gtk.main() if confwin.daemon_enabled: try: # the launched daemon shuts down # an already running daemon print "Launching Mailnag daemon." subprocess.Popen(os.path.join(BIN_DIR, "mailnag")) except: print "ERROR: Failed to launch Mailnag daemon." else: DBusGMainLoop(set_as_default = True) # shutdown running Mailnag daemon shutdown_existing_instance() if __name__ == "__main__": main() mailnag-1.2.1/po/000077500000000000000000000000001272232533200135425ustar00rootroot00000000000000mailnag-1.2.1/po/cs.po000066400000000000000000000165701272232533200145200ustar00rootroot00000000000000# Czech translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2016-03-30 13:27+0000\n" "Last-Translator: Radek Otáhal \n" "Language-Team: Czech \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" "Language: cs\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Uživatelský skript" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Spuštění uživatelem definovaného skriptu při přijetí e-mailu." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "účet" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "odesílatel" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "předmět" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" "Následující skript bude spuštěn při novém e-mailu.\n" "Mailnag předá celkové počet nových e-mailů tomuto skriptu,\n" "následovaný %s sekvencemi." #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Zobrazí nove e-maily v Ubuntu Messaging menu." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maximální počet viditelných e-mailů:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Spam filtr" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtruje nevyžádané e-maily." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag bude ignorovat e-maily obsahující aspoň jedno z \n" "následujících slov v poli předmět nebo odesílatel." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "LibNotify notifikace" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Zobrazí popup okno při novém e-mailu." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Počet nových e-mailů" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "Krátký přehled nových e-mailů" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "Detailní přehled nových e-mailů" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Jedna notifikace pro nový e-mail" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Notifikační mód:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} nových e-mailů" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "z {0} a další." #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "z {0}." #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Nový mail" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(a {0} dalších)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Označit jako přečtený" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Zvukové notifikace" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Přehraje zvuk při novém e-mailu." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "GNOME Online účty" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Integrace GNOME Online účtů." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "DBus Service" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Vystaví Mailnag funkcionality prostřednictvím DBus služby." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Žádný předmět" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Konfigurace pluginu" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Povoleno" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Název" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Odstranit tento účet:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "volitelně" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Jiný (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Jiný (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "Spojení selhalo." #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Poštovní účet" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Povolit Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Povolit SSL encryption" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Název účtu:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Typ účtu:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Uživatel:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Heslo:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Server:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Port:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "Složky (volitelně)" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Nastavení Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Obecné" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Účty" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Pluginy" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "a přispěvatelé." #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Přidat účet" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Odebrat účet" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Upravit účet" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Upravit plugin" mailnag-1.2.1/po/de.po000066400000000000000000000167141272232533200145030ustar00rootroot00000000000000# German translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2016-04-13 18:08+0000\n" "Last-Translator: Patrick Ulbrich \n" "Language-Team: German \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Benutzerskript" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Führt ein benutzerdefiniertes Skript bei E-Mail-Ankunft aus." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "Konto" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "Absender" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "Betreff" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" "Das folgende Skript wird immer dann ausgeführt, wenn neue E-Mails " "eintreffen.\n" "Mailnag überreicht die Anzahl der neuen E-Mails an dieses Skript,\n" "gefolgt von %s Sequenzen." #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Zeigt neue E-Mails in Ubuntus Benachrichtigungsmenü an." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maximale Anzahl sichtbarer neuer E-Mails:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Spamfilter" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtert unerwünschte E-Mails heraus." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag ignoriert E-Mails, die mindestens eines der \n" "folgenden Wörter im Betreff oder Sender enthalten." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "LibNotify-Benachrichtigungen" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Zeigt eine Benachrichtigung an, wenn neue E-Mails eintreffen." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Anzahl neuer E-Mails" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "Kurze Zusammenfassung neuer E-Mails" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "Detaillierte Zusammenfassung neuer E-Mails" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Eine Benachrichtigung pro neuer E-Mail" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Benachrichtigungsmodus:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} neue Mails" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "von {0} und anderen." #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "von {0}." #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Neue Nachricht" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(und {0} weitere)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Als gelesen markieren" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Klangbenachrichtigungen" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Spielt einen Klang ab, wenn neue E-Mails eintreffen." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "GNOME Online Accounts" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "GNOME Online Accounts Integration." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "DBus-Dienst" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Stellt Mailnags Funktionalität durch einen DBus-Dienst bereit." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Kein Betreff" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Plugin-Konfiguration" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Aktiviert" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Name" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Dieses Konto löschen:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "optional" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Anderer (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Anderer (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "Verbindung fehlgeschlagen." #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "E-Mail-Konto" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Push-IMAP aktivieren" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "SSL-Verschlüsselung aktivieren" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Kontoname:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Kontotyp:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Benutzer:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Passwort:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Server:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Port:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "Ordner (optional)" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Mailnag-Konfiguration" #: data/config_window.ui.h:2 msgid "General" msgstr "Allgemein" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Konten" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Plugins" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" "Mailnag - Ein erweiterbarer " "Benachrichtigungsdienst.\n" "Urheberrecht (c) 2011 - 2016 Patrick Ulbrich\n" "und Mitwirkende." #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Konto hinzufügen" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Konto löschen" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Konto bearbeiten" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Plugin bearbeiten" mailnag-1.2.1/po/es.po000066400000000000000000000160351272232533200145160ustar00rootroot00000000000000# Spanish translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2015-02-15 22:50+0000\n" "Last-Translator: dagavi \n" "Language-Team: Spanish \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Secuencia de órdenes de usuario" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Ejecuta un «script» definido por el usuario al llegar un mensaje." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "remitente" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "asunto" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Muestra los mensajes nuevos en el menú de mensajería de Ubuntu." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Cantidad máxima de mensajes visibles:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Filtro de correo no deseado" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtra correos no deseados." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag ignorará los mensajes que contengan por lo menos\n" "una de las palabras siguientes en el asunto o remitente." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Notificaciones con LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Muestra un cuadro emergente al llegar mensajes nuevos." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Contador de mensajes nuevos" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Una notificación por mensaje nuevo" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Modo de notificación:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} mensajes nuevos" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Mensaje nuevo" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(y {0} más)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Marcar como leído" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Notificaciones con sonido" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Reproduce un sonido al llegar un mensaje nuevo." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "Cuentas en línea de GNOME" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Integración con las cuentas en línea de GNOME." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Servicio DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Expone la funcionalidad de Mailnag a través de un servicio D-Bus." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Sin asunto" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Configuración de complementos" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Activado" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Nombre" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Eliminar esta cuenta:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Otro (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Otro (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Cuenta de correo" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Activar Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Activar cifrado SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Nombre de la cuenta:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Tipo de cuenta:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Usuario:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Contraseña:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Servidor:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Puerto:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Configuración de Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "General" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Cuentas" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Complementos" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Añadir una cuenta" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Eliminar la cuenta" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Editar la cuenta" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Editar el complemento" mailnag-1.2.1/po/fr.po000066400000000000000000000161161272232533200145160ustar00rootroot00000000000000# French translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: Philippe Poumaroux \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2015-11-19 18:40+0000\n" "Last-Translator: Jean-Marc \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" "Language: \n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Script utilisateur" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "" "Exécute un script défini par l'utilisateur lors d'un nouveau courriel." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "expéditeur" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "objet" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Affiche les nouveaux courriels dans le menu des messages d'Ubuntu." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Nombre maximum de courriels visibles :" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Filtre anti-pourriel" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtre les courriels non désirés." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag ignorera tous les courriels qui contiennent au moins \n" "un des mots suivants dans l'objet ou l'expéditeur." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Notifications LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "" "Affiche une fenêtre de notification à l'arrivée d'un nouveau courriel." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Nombre de nouveaux courriels" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Une notification par nouveau courriel" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Mode de notification :" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} nouveaux courriels" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Nouveau courriel" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(et {0} de plus)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Marquer comme lu" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Notifications sonores" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Joue un son à l'arrivée d'un nouveau courriel." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "Comptes en ligne GNOME" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Integration des comptes en ligne GNOME." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Service DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Rend disponible les fonctionnalités de Mailnag via un service DBus." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Sans objet" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Configuration du greffon" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Activé" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Nom" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Supprimer ce compte :" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "facultatif" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Autre (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Autre (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Compte de messagerie" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Activer le Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Activer le chiffrage SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Nom du compte :" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Type de compte :" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Utilisateur :" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Mot de passe :" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Serveur :" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Port :" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Configuration de Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Général" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Comptes" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Greffons" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Ajouter un compte" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Supprimer le compte" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Modifier le compte" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Modifier le plugin" mailnag-1.2.1/po/gl.po000066400000000000000000000154471272232533200145170ustar00rootroot00000000000000# Galician translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2014-10-05 08:38+0000\n" "Last-Translator: Marcos Lans \n" "Language-Team: Galician \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Script do usuario" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Executa un script do usuario ao recibir algún correo." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "remitente" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "asunto" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Número máximo de mensaxes visibles:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Filtro de spam" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtra os correos non desexados." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag ignorará os correos que conteñan alomenos \n" "unha das seguintes palabras no asunto ou no remitente." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Notificacións LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Mostra unha xanela emerxente ao recibir correos novos." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Total de novos correos" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Unha notificación por cada novo correo" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Modo Notificación:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} novos correos" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Correo novo" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(e {0} máis)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Marcar como lido" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Notificacións con son" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Reproduce un son ao recibir un novo correo-e" #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Servizo DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Expón a funcionalidade Mailnag mediante un servizo DBus." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Sen asunto" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Configuración do complemento" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Activado" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Nome" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Eliminar esta conta:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Outro (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Outro (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Conta de correo-e" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Activar Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Activar cifrado SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Nome da conta:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Tipo de conta:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Usuario:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Contrasinal:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Servidor:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Porto:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Configuración de Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Xeral" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Contas" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Complementos" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Engadir conta" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Retirar conta" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Editar conta" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Editar complemento" mailnag-1.2.1/po/he.po000066400000000000000000000147761272232533200145150ustar00rootroot00000000000000# Hebrew translation for mailnag # Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2012. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2015-05-01 05:58+0000\n" "Last-Translator: AmiG \n" "Language-Team: Hebrew \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "הצגת הודעות דוא\"ל חדשות בתפריט המסרים של אובונטו." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "מספר מקסימלי של הודעות דוא\"ל ניראות:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "" #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "אופן התראות:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(ועוד {0})" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "סימון כנקרא" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "התראות קוליות" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "השמעל צליל עם הגעת דוא\"ל חדש." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "חשבונות GNOME מקוונים" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "אינטגרציה של חשבונות GNOME מקוונים." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "שרות DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "" #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "ללא נושא" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "מאופשר" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "שם" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "מחיקת החשבון:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "אופציונאלי" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "חשבון דוא\"ל" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "איפשור דחיפת IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "איפשור הצפנת SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "שם חשבון:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "סוג חשבון:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "משתמש:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "סיסמא:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "שרת:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "יציאה:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "הגדרות התוכנה" #: data/config_window.ui.h:2 msgid "General" msgstr "כללי" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "חשבונות" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "" mailnag-1.2.1/po/id.po000066400000000000000000000161531272232533200145040ustar00rootroot00000000000000# Indonesian translation for mailnag # Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2015. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2016-01-12 16:32+0000\n" "Last-Translator: zmni \n" "Language-Team: Indonesian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" "Language: id\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Skrip Pengguna" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Jalankan skrip tentuan pengguna saat surat datang." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "pengirim" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "subyek" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Tampilkan surat baru di menu Ubuntu Messaging." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Jumlah maksimum surat terlihat:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Penyaring Spam" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Saring surat yang tidak diinginkan." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag akan mengabaikan surat yang memuat sedikitnya \n" "satu dari kata-kata berikut di dalam subyek atau pengirim." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Notifikasi LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Tampilkan jendela munculan ketika surat baru datang." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Jumlah surat baru" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "Ringkasan singkat surel baru" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "Ringkasan detail surel baru" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Satu notifikasi per surat baru" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Mode notifikasi:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} surat baru" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "dari {0} dan lainnya." #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "dari {0}." #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Surat baru" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(dan {0} lainnya)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Tandai sudah dibaca" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Notifikasi Suara" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Putar suara ketika surat baru datang." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "Akun Daring GNOME" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Integrasi Akun Daring GNOME." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Layanan DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Singkap fungsionalitas Mailnag melalui layanan DBus." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Tidak ada subyek" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Konfigurasi Plugin" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Aktifkan" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Nama" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Hapus akun ini:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "opsional" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Lainnya (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Lainnya (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "Koneksi gagal." #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Akun Surat" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Aktifkan Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Aktifkan Enkripsi SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Nama akun:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Jenis akun:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Pengguna:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Sandi:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Server:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Port:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "Folder (opsional)" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Konfigurasi Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Umum" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Akun" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Plugin" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" "Mailnag - Sebuah daemon " "notifikasi surel yang dapat diperluas.\n" "Hak cipta (c) 2011 - 2016 Patrick Ulbrich\n" "dan kontributor." #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Tambah Akun" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Buang Akun" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Sunting Akun" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Sunting Plugin" mailnag-1.2.1/po/it.po000066400000000000000000000155571272232533200145330ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2015-02-15 17:55+0000\n" "Last-Translator: Patrick Ulbrich \n" "Language-Team: Italian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" "Language: Italian\n" "X-Loco-Source-Locale: it_IT\n" "X-Loco-Parser: loco_parse_po\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Loco-Target-Locale: it_IT\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Script utente" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Esegue uno script definito dall'utente all'avvio." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "Mittente" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "Oggetto" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Mostra nuove mail nel menù Messaggistica di Ubuntu" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Massimo numero di mail visibili:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Filtro spam" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtra le mail indesiderate." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag ignorerà le mail contenenti almeno una delle \n" "\n" "seguenti parole nell'oggetto o nel mittente." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Notifiche LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Mostra un popup all'arrivo di nuova posta." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Numero di nuove mail" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Una notifica ogni nuova mail" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Modalità di notifica:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} nuove mail" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Nuova mail" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(e altri {0})" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Contrassegna come \\\"già letto\\\"" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Notifiche audio" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Riproduci un suono all'arrivo di nuova posta." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "GNOME Online Accounts" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Integrazione con GNOME Online Accounts" #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Servizio DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Espone le funzioni di Mailnag tramite un servizio DBus." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Nessun oggetto" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Configurazione plugin" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Attivo" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Nome" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Eliminare questo account:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "opzionale" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Altro (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Altro (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Account email" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Attiva il protocollo Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Attiva il protocollo crittografico SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Nome dell'account:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Tipo di account:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Nome utente:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Password:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Server:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Porta:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Configurazione di Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Impostazioni generali" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Account" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Plugin" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Aggiungi account" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Rimuovi account" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Modifica account" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Modifica plugin" mailnag-1.2.1/po/mailnag.pot000066400000000000000000000134061272232533200157020ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "" #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "" #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "" #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "" #: data/config_window.ui.h:2 msgid "General" msgstr "" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "" mailnag-1.2.1/po/pl.po000066400000000000000000000147651272232533200145320ustar00rootroot00000000000000# Polish translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2014-07-16 19:54+0000\n" "Last-Translator: Piotr Filipek \n" "Language-Team: Polish \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Skrypt użytkownika" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "nadawca" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "temat" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maksymalna ilość widocznych wiadomości:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Filtr spamowy" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "" #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Powiadomienia LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Pokaż okno kiedy przychodzi nowa poczta." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Liczba nowych wiadomości" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Jedno powiadomienie dla jednej wiadomości" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Tryb powiadamiania:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(i {0} więcej)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Oznacz jako przeczytana" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Dżwięk powiadomienia" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Odtwarzaj dźwięk kiedy przychodzi nowa poczta." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Usługa DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "" #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Brak tematu" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Konfiguracja wtyczki" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Włączone" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Nazwa" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Usuń to konto:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "opcjonalny" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Inny (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Inny (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Konto pocztowe" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Włącz Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Włącz szyfrowanie SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Nazwa konta:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Typ konta:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Użytkownik:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Hasło:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Serwer:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Port:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Konfiguracja Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Ogólne" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Konta" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Wtyczki" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Dodaj konto" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Usuń konto" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Zmodyfikuj konto" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Zmodyfikuj wtyczkę" mailnag-1.2.1/po/pt.po000066400000000000000000000165711272232533200145370ustar00rootroot00000000000000# Portuguese translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2016-05-21 16:10+0000\n" "Last-Translator: Pedro Beja \n" "Language-Team: Portuguese \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Script de Utilizador" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Executa um script definido pelo utilizador na receção de mails." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "conta" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "remetente" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "assunto" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" "O script seguinte vai ser executado sempre que cheguem novos mails.\n" "O Mailnag passa a quantidade total de mails novos para este script,\n" "seguido por sequências de %s." #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Mostra mails novos no menu de mensagens do Ubuntu." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Número máximo de mails visíveis:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Filtro Spam" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtra mails indesejados." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "O Mailnag irá ignorar mails que contenham pelo menos uma \n" "das palavras seguintes no assunto ou remetente." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Notificações da LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Mostra um popup quando chegam novos mails." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Quantidade de mails novos" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "Resumo curto de mails novos" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "Resumo detalhado de mails novos" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Uma notificação por novo mail" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Modo de notificação:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} novos mails" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "de {0} e outros." #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "de {0}." #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Novo mail" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(e mais {0})" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Marcar como lido" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Notificações de Som" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Toca um som quando chegam novos mails." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "Contas Online no GNOME" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Integração de Contas Online no GNOME." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Serviço DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Expor a funcionalidade do Mailnag através do serviço DBus." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Sem assunto" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Configuração de Plugin" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Ativado" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Nome" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Apagar esta conta:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Outro (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Outro (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "A ligação falhou." #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Conta de mail" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Ativar Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Ativar encriptação SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Nome da Conta:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Tipo de conta:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Utilizador:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Password:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Servidor:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Porta:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "Pastas (opcional)" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Configuração do Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Geral" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Contas" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Plugins" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" "Mailnag - Um daemon de " "notificação de correio extensível.\n" "Direitos de autor (c) 2011 - 2016 Patrick Ulbrich\n" "e contribuidores." #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Adicionar Conta" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Remover Conta" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Editar Conta" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Editar Plugin" mailnag-1.2.1/po/pt_BR.po000066400000000000000000000166141272232533200151200ustar00rootroot00000000000000# Brazilian Portuguese translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2011. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2016-05-21 16:11+0000\n" "Last-Translator: Pedro Beja \n" "Language-Team: Brazilian Portuguese \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Script de Usuário" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Executa um script definido pelo usuário na receção de mails." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "conta" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "remetente" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "assunto" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" "O script seguinte vai ser executado sempre que cheguem novos mails.\n" "O Mailnag passa a quantidade total de mails novos para este script,\n" "seguido por sequências de %s." #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Mostra mails novos no menu de mensagens do Ubuntu." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Número máximo de mails visíveis:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Filtro Spam" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtra mails indesejados." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "O Mailnag irá ignorar mails que contenham pelo menos uma \n" "das palavras seguintes no assunto ou remetente." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Notificações da LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Mostra um popup quando chegam novos mails." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Quantidade de mails novos" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "Resumo curto de mails novos" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "Resumo detalhado de mails novos" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Uma notificação por novo mail" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Modo de notificação:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} novos mails" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "de {0} e outros." #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "de {0}." #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Novo mail" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(e mais {0})" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Marcar como lido" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Notificações de Som" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Toca um som quando chegam novos mails." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "Contas Online no GNOME" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Integração de Contas Online no GNOME." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Serviço DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Expor a funcionalidade do Mailnag através do serviço DBus." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Sem assunto" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Configuração de Plugin" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Ativado" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Nome" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Apagar esta conta:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Outro (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Outro (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "A ligação falhou." #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Conta de mail" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Habilitar Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Habilitar criptografia SSL" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Nome da conta:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Tipo de conta:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Usuário:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Senha:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Servidor:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Porta:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "Pastas (opcional)" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Configuração do Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Geral" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Contas" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Plugins" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" "Mailnag - Um daemon de " "notificação de correio extensível.\n" "Direitos de autor (c) 2011 - 2016 Patrick Ulbrich\n" "e contribuidores." #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Adicionar Conta" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Remover Conta" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Editar Conta" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Editar Plugin" mailnag-1.2.1/po/sr.po000066400000000000000000000204461272232533200145340ustar00rootroot00000000000000# Serbian translation for mailnag # Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 # This file is distributed under the same license as the mailnag package. # Мирослав Николић , 2013—2016. msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2016-01-29 04:27+0000\n" "Last-Translator: Мирослав Николић \n" "Language-Team: Serbian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" "Language: sr\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Корисничка скрипта" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Покреће скрипту коју је одредио корисник када пристигне пошта." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "пошиљалац" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "тема" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Убунту Јунити" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Приказује нове поштанске поруке у Убунтуовом изборнику дописивања." #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Највећи број видљивих поштанских порука:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Пропусник спама" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Издваја нежељену пошту." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Поштарко ће занемарити пошту која садржи барем једну \n" "од следећих речи у теми или пошиљаоцу." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Обавештења либнотифија" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Приказује облачић када стигне нова пошта." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Број нових поштанских порука" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "Кратак сажетак нове поште" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "Опширан сажетак нове поште" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Једно обавештење по новој пошти" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Режим обавештавања:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "нових поштанских порука: {0}" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "шаље {0} и други." #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "шаље {0}." #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Нова пошта" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(и још {0})" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Означи као прочитано" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Звучно обавештавање" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Пушта звук када стигне нова пошта." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "Гномови налози на мрежи" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Обједињавање Гномових налога на мрежи." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Услуга Д-сабирнице" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Излаже функционалност Поштарка путем услуге Д-сабирнице." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Без теме" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Подешавање прикључка" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Укључено" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Назив" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Обриши ове налоге:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "изборно" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Друго (ИМАП)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Друго (ПОП3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "Веза није успела." #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Налог поште" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Укључи Гурни-ИМАП" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Укључи ССЛ шифровање" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Назив налога:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Врста налога:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Корисник:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Лозинка:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Сервер:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Прикључник:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "Фасцикле (изборно)" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Подешавања Поштарка" #: data/config_window.ui.h:2 msgid "General" msgstr "Опште" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Налози" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Прикључци" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" "Поштарко — Прошириви " "услужник обавештавања о пошти.\n" "Ауторска права © 2011—2016 Патрик Улбрих\n" "и доприносиоци." #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Додај налог" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Уклони налог" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Уреди налог" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Уреди прикључак" mailnag-1.2.1/po/sv.po000066400000000000000000000165471272232533200145470ustar00rootroot00000000000000# Swedish translation for mailnag # Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2014. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2016-03-25 21:04+0000\n" "Last-Translator: Anders Jonsson \n" "Language-Team: Swedish \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Användarskript" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Kör ett användardefinerat skript vid inkommande meddelanden." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "konto" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "avsändare" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "ämne" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" "Följande skript kommer köras när nya meddelanden anländer.\n" "Mailnag skickar det totala antalet nya meddelanden till detta skript,\n" "följt av sekvenser av %s." #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "Ubuntu Unity" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "Visar nya meddelanden i Ubuntus Meddelandemeny" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maximalt antal visade meddelanden:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Skräpfilter" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Filtrera bort oönskade mail." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag kommer att ignorera meddelanden som innehåller minst ett av \n" "följande ord i ämne eller avsändare." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "LibNotify-aviseringar" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Visar en notis på skärmen när nya meddelanden anländer." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Antal nya meddelanden" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "Kort summering av nya meddelanden" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "Detaljerad summering av nya meddelanden" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "En avisering per nytt meddelande" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Aviseringsläge" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} nya meddelanden" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "från {0} och andra." #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "från {0}." #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Nytt meddelande" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(och {0} till)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Markera som läst" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Ljudaviseringar" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Spelar upp ett ljud när nya meddelanden anländer." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "GNOME Nätkonton" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "Integrering med GNOME Nätkonton." #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "DBus-tjänst" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Visar Mailnags funktioner via en DBus-tjänst." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Ämne saknas" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Inställning av insticksprogram" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Aktiverad" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Namn" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Ta bort detta konto:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "valfritt" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Annan (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Annan (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "Anslutning misslyckades." #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "E-postkonto" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Aktivera Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Aktivera SSL-kryptering" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Kontonamn:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Kontotyp:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Användare:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Lösenord:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Server:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Port:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "Mappar (valfritt)" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Mailnag-inställningar" #: data/config_window.ui.h:2 msgid "General" msgstr "Allmänt" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Konton" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Tillägg" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" "Mailnag - En utökningsbar e-" "postaviseringsdemon.\n" "Copyright © 2011 - 2016 Patrick Ulbrich\n" "och bidragsgivare." #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Lägg till Konto" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Ta bort konto" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Redigera konto" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Redigera instick" mailnag-1.2.1/po/tr.po000066400000000000000000000152431272232533200145340ustar00rootroot00000000000000# Turkish translation for mailnag # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2014-07-30 18:44+0000\n" "Last-Translator: Patrick Ulbrich \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" "Language: \n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Kullanıcı Script" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Mail geldiğinde kullanıcı tanımlı scripti çalıştır." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "gönderen" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "konu" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Görüntülenecek en fazla mail sayısı" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Spam filtresi" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "İstenmeyen postaları filtreler." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag belirtilen kelimelerin konu veya gönderende\n" "geçmesi durumunda mailleri görmezden gelecektir." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "LibNotify Uyarıları" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Yeni mail geldiğinde pencerede göster." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Yeni mail sayısı" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Her yeni mail için bir uyarı" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Uyarı kipi:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "ve {0} fazlası" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Okundu olarak işaretle" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Sesli Uyarı" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Yeni mail geldiğinde ses çal." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "DBus Servisi" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "DBus servisi ile Mailnag işlevselliğini ortaya koyar." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Konusuz" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Eklenti Ayarları" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Etkin" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Adı" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Bu hesabı sil:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "opsiyonel" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Diğer (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Diğer (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Mail Hesabı" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Push-IMAP Etkin" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "SSL şifreleme Erkin" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Hesap adı:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Hesap türü:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Kullanıcı:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Parola:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Sunucu:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Port:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Mailnag Ayarları" #: data/config_window.ui.h:2 msgid "General" msgstr "Genel" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Hesaplar" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Eklentiler" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Hesap Ekle" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Hesabı Sil" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Hesabı Düzenle" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Eklentiyi Düzenle" mailnag-1.2.1/po/uk.po000066400000000000000000000167541272232533200145360ustar00rootroot00000000000000# Ukrainian translation for mailnag # This file is distributed under the same license as the mailnag package. # Rax G , 2012. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2015-01-30 17:58+0000\n" "Last-Translator: Oleg «Eleidan» Kulyk \n" "Language-Team: Ukrainian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "Користувацький скріпт" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "Виконувати скріпт користувача при отриманні листа." #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "відправник" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "тема" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Максимальна к-сть листів у списку:" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "Фільтр спаму" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "Відфільтровує небажані листі." #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag буде ігнорувати листи, що містять в темі\n" "чи імені відправника хоча б одне слово з переліку ." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Сповіщення LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Сповіщає про нові листи за допомогою випливаючих повідомлень." #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "Кількість нових листів" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "Про кожен лист окремо" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "Метод сповіщення:" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} листів" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "Новий лист" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "(і ще {0})" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "Позначити як прочитане" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "Звукові сповіщення" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "Супроводжує нові листи звуковим сповіщенням." #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "Служба DBus" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "Забезпечує синхронізацію зі службою DBus." #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "Без теми" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Налаштування додатка" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "Задіяно" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "Назва" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "Видалити обліковий запис" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "(необов’язково)" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "Інше (IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "Інше (POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "Поштовий обліковий запис" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "Задіяти Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "Задіяти SSL-шифрування" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "Назва пошти" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "Поштовий сервіс:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "Користувач:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "Пароль:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "Сервер:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "Порт" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Налаштування Mailnag" #: data/config_window.ui.h:2 msgid "General" msgstr "Загальне" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "Облікові записи" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "Додатки" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "Додати обліковий запис" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "Видалити обліковий запис" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "Редагувати обліковий запис" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "Налаштувати додаток" mailnag-1.2.1/po/zh_CN.po000066400000000000000000000147201272232533200151070ustar00rootroot00000000000000# Chinese (Simplified) translation for mailnag # Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2012. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2014-07-14 11:16+0000\n" "Last-Translator: Hu Meng \n" "Language-Team: Chinese (Simplified) \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "用户脚本" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "运行在邮件到达时的用户自定义脚本。" #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "寄件人" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "主题" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "可显示邮件的最大数量" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "垃圾邮件过滤器" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "过滤掉不想要的邮件。" #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "弹出窗口通知" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "当新邮件到达时弹出。" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "新邮件数量" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "每封新邮件通知一次" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "通知模式" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "( 还有 {0} 条)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "标记为已读" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "声音通知" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "当新邮件到达时播放声音。" #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "DBus服务" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "" #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "无主题" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "已启用" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "名称" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "删除该帐户:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "可选" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "其他(IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "其他(POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "邮件账户" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "启用 Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "启用 SSL 加密" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "账户名称:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "账户类型:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "用户名:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "密码:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "服务器:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "端口:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Mailnag 配置" #: data/config_window.ui.h:2 msgid "General" msgstr "基本设置" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "帐户" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "" mailnag-1.2.1/po/zh_TW.po000066400000000000000000000153271272232533200151450ustar00rootroot00000000000000# Chinese (Traditional) translation for mailnag # Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2012. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2016-02-03 19:08+0100\n" "PO-Revision-Date: 2015-04-23 16:45+0000\n" "Last-Translator: elleryq \n" "Language-Team: Chinese (Traditional) \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-05-26 15:48+0000\n" "X-Generator: Launchpad (build 18053)\n" #: Mailnag/plugins/userscriptplugin.py:62 msgid "User Script" msgstr "用戶腳本" #: Mailnag/plugins/userscriptplugin.py:63 msgid "Runs an user defined script on mail arrival." msgstr "運行在郵件到達時的用戶自定義腳本。" #: Mailnag/plugins/userscriptplugin.py:83 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:83 msgid "sender" msgstr "寄件人" #: Mailnag/plugins/userscriptplugin.py:83 msgid "subject" msgstr "主題" #: Mailnag/plugins/userscriptplugin.py:84 #, python-format msgid "" "The following script will be executed whenever new mails arrive.\n" "Mailnag passes the total count of new mails to this script,\n" "followed by %s sequences." msgstr "" #: Mailnag/plugins/unityplugin.py:98 msgid "Ubuntu Unity" msgstr "" #: Mailnag/plugins/unityplugin.py:99 msgid "Shows new mails in Ubuntu's Messaging menu." msgstr "在 Ubuntu 的訊息選單裡顯示新郵件" #: Mailnag/plugins/unityplugin.py:118 msgid "Maximum number of visible mails:" msgstr "可顯示郵件的最大數量" #: Mailnag/plugins/spamfilterplugin.py:69 msgid "Spam Filter" msgstr "垃圾郵件過濾器" #: Mailnag/plugins/spamfilterplugin.py:70 msgid "Filters out unwanted mails." msgstr "過濾掉不想要的郵件。" #: Mailnag/plugins/spamfilterplugin.py:90 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "Mailnag 將會忽略主旨或寄件者裡帶有這些字詞的郵件。" #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "彈出視窗通知" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "當新郵件到達時彈出訊息視窗。" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Count of new mails" msgstr "新郵件數量" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:120 msgid "One notification per new mail" msgstr "每封新郵件通知一次" #: Mailnag/plugins/libnotifyplugin.py:128 msgid "Notification mode:" msgstr "通知模式" #: Mailnag/plugins/libnotifyplugin.py:215 #: Mailnag/plugins/libnotifyplugin.py:251 #: Mailnag/plugins/libnotifyplugin.py:279 #, python-brace-format msgid "{0} new mails" msgstr "{0} 封新郵件" #: Mailnag/plugins/libnotifyplugin.py:217 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:219 #: Mailnag/plugins/libnotifyplugin.py:222 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:221 #: Mailnag/plugins/libnotifyplugin.py:253 #: Mailnag/plugins/libnotifyplugin.py:281 msgid "New mail" msgstr "新郵件" #: Mailnag/plugins/libnotifyplugin.py:246 #: Mailnag/plugins/libnotifyplugin.py:248 #, python-brace-format msgid "(and {0} more)" msgstr "( 還有 {0} 條)" #: Mailnag/plugins/libnotifyplugin.py:268 msgid "Mark as read" msgstr "標記爲已讀" #: Mailnag/plugins/soundplugin.py:65 msgid "Sound Notifications" msgstr "聲音通知" #: Mailnag/plugins/soundplugin.py:66 msgid "Plays a sound when new mails arrive." msgstr "當新郵件到達時播放聲音。" #: Mailnag/plugins/goaplugin.py:93 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:94 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/plugins/dbusplugin.py:83 msgid "DBus Service" msgstr "DBus服務" #: Mailnag/plugins/dbusplugin.py:84 msgid "Exposes Mailnag's functionality via a DBus service." msgstr "允許透過DBus服務使用Mailnag的功能" #: Mailnag/daemon/mails.py:171 msgid "No subject" msgstr "無主題" #: Mailnag/configuration/plugindialog.py:33 msgid "Plugin Configuration" msgstr "Plugin 配置" #: Mailnag/configuration/configwindow.py:109 #: Mailnag/configuration/configwindow.py:134 #: Mailnag/configuration/accountdialog.py:88 msgid "Enabled" msgstr "已啓用" #: Mailnag/configuration/configwindow.py:115 #: Mailnag/configuration/configwindow.py:141 #: Mailnag/configuration/accountdialog.py:94 msgid "Name" msgstr "名稱" #: Mailnag/configuration/configwindow.py:344 msgid "Delete this account:" msgstr "刪除該帳戶:" #: Mailnag/configuration/accountdialog.py:84 msgid "optional" msgstr "可選" #: Mailnag/configuration/accountdialog.py:177 msgid "Other (IMAP)" msgstr "其他(IMAP)" #: Mailnag/configuration/accountdialog.py:178 msgid "Other (POP3)" msgstr "其他(POP3)" #: Mailnag/configuration/accountdialog.py:271 msgid "Connection failed." msgstr "" #: data/account_dialog.ui.h:1 msgid "Mail Account" msgstr "郵件賬戶" #: data/account_dialog.ui.h:2 msgid "Enable Push-IMAP" msgstr "啓用 Push-IMAP" #: data/account_dialog.ui.h:3 msgid "Enable SSL encryption" msgstr "啓用 SSL 加密" #: data/account_dialog.ui.h:4 msgid "Accountname:" msgstr "賬戶名稱:" #: data/account_dialog.ui.h:5 msgid "Account type:" msgstr "賬戶類型:" #: data/account_dialog.ui.h:6 msgid "User:" msgstr "用戶名:" #: data/account_dialog.ui.h:7 msgid "Password:" msgstr "密碼:" #: data/account_dialog.ui.h:8 msgid "Server:" msgstr "伺服器:" #: data/account_dialog.ui.h:9 msgid "Port:" msgstr "通訊埠:" #: data/account_dialog.ui.h:10 msgid "Folders (optional)" msgstr "" #: data/config_window.ui.h:1 msgid "Mailnag Configuration" msgstr "Mailnag 配置" #: data/config_window.ui.h:2 msgid "General" msgstr "基本設置" #: data/config_window.ui.h:3 msgid "Accounts" msgstr "帳戶" #: data/config_window.ui.h:4 msgid "Plugins" msgstr "" #: data/config_window.ui.h:5 msgid "" "Mailnag - An extensible mail " "notification daemon.\n" "Copyright (c) 2011 - 2016 Patrick Ulbrich\n" "and contributors." msgstr "" #: data/config_window.ui.h:8 msgid "Add Account" msgstr "增加帳戶" #: data/config_window.ui.h:9 msgid "Remove Account" msgstr "刪除帳戶" #: data/config_window.ui.h:10 msgid "Edit Account" msgstr "編輯帳戶" #: data/config_window.ui.h:11 msgid "Edit Plugin" msgstr "編輯 Plugin" mailnag-1.2.1/setup.py000077500000000000000000000105501272232533200146420ustar00rootroot00000000000000#!/usr/bin/env python2 # To install Mailnag run this script as root: # ./setup.py install from distutils.core import setup from distutils.cmd import Command from distutils.log import warn, info, error from distutils.command.install_data import install_data from distutils.command.build import build from distutils.sysconfig import get_python_lib import sys import os import subprocess import glob import shutil from Mailnag.common.dist_cfg import PACKAGE_NAME, APP_VERSION # TODO : This hack won't work with --user and --home options PREFIX = '/usr' for arg in sys.argv: if arg.startswith('--prefix='): PREFIX = arg[9:] BUILD_DIR = 'build' for arg in sys.argv: if arg.startswith('--build-base='): BUILD_DIR = arg[13:] BUILD_LOCALE_DIR = os.path.join(BUILD_DIR, 'locale') BUILD_PATCH_DIR = os.path.join(BUILD_DIR, 'patched') INSTALL_LIB_DIR = os.path.join(get_python_lib(prefix=PREFIX), 'Mailnag') class BuildData(build): def run (self): # generate translations try: rc = subprocess.call('./gen_locales ' + BUILD_LOCALE_DIR, shell = True) if (rc != 0): if (rc == 1): err = "MKDIR_ERR" elif (rc == 2): err = "MSGFMT_ERR" else: err = "UNKNOWN_ERR" raise Warning, "gen_locales returned %d (%s)" % (rc, err) except Exception, e: error("Building locales failed.") error("Error: %s" % str(e)) sys.exit(1) # remove patch dir (if existing) shutil.rmtree(BUILD_PATCH_DIR, ignore_errors = True) # copy mailnag source to build dir for patching purposes shutil.copytree('Mailnag/common', os.path.join(BUILD_PATCH_DIR, 'common')) # patch paths self._patch_file('./data/mailnag.desktop', os.path.join(BUILD_PATCH_DIR, 'mailnag.desktop'), '/usr', PREFIX) self._patch_file('./data/mailnag-config.desktop', os.path.join(BUILD_PATCH_DIR, 'mailnag-config.desktop'), '/usr', PREFIX) self._patch_file(os.path.join(BUILD_PATCH_DIR, 'common/dist_cfg.py'), os.path.join(BUILD_PATCH_DIR, 'common/dist_cfg.py'), './locale', os.path.join(PREFIX, 'share/locale')) self._patch_file(os.path.join(BUILD_PATCH_DIR, 'common/dist_cfg.py'), os.path.join(BUILD_PATCH_DIR, 'common/dist_cfg.py'), './data', os.path.join(PREFIX, 'share/applications')) self._patch_file(os.path.join(BUILD_PATCH_DIR, 'common/dist_cfg.py'), os.path.join(BUILD_PATCH_DIR, 'common/dist_cfg.py'), './Mailnag', os.path.join(PREFIX, INSTALL_LIB_DIR)) self._patch_file(os.path.join(BUILD_PATCH_DIR, 'common/dist_cfg.py'), os.path.join(BUILD_PATCH_DIR, 'common/dist_cfg.py'), "'.'", "'%s'" % os.path.join(PREFIX, 'bin')) build.run (self) def _patch_file(self, infile, outfile, orig, replaced): with open(infile, 'r') as f: strn = f.read() strn = strn.replace(orig, replaced) with open(outfile, 'w') as f: f.write(strn) class InstallData(install_data): def run (self): self._add_locale_data() self._add_icon_data() install_data.run (self) def _add_locale_data(self): for root, dirs, files in os.walk(BUILD_LOCALE_DIR): for file in files: src_path = os.path.join(root, file) dst_path = os.path.join('share/locale', os.path.dirname(src_path[len(BUILD_LOCALE_DIR)+1:])) self.data_files.append((dst_path, [src_path])) def _add_icon_data(self): for root, dirs, files in os.walk('data/icons'): for file in files: src_path = os.path.join(root, file) dst_path = os.path.join('share/icons', os.path.dirname(src_path[len('data/icons')+1:])) self.data_files.append((dst_path, [src_path])) class Uninstall(Command): def run (self): # TODO pass setup(name=PACKAGE_NAME, version=APP_VERSION, description='An extensible mail notification daemon', author='Patrick Ulbrich', author_email='zulu99@gmx.net', url='https://github.com/pulb/mailnag', license='GNU GPL2', package_dir = {'Mailnag.common' : os.path.join(BUILD_PATCH_DIR, 'common')}, packages=['Mailnag', 'Mailnag.common', 'Mailnag.configuration', 'Mailnag.daemon', 'Mailnag.plugins'], scripts=['mailnag', 'mailnag-config'], data_files=[('share/mailnag', glob.glob('data/*.ui')), ('share/mailnag', ['data/config_window.css']), ('share/mailnag', ['data/mailnag.ogg']), ('share/mailnag', ['data/mailnag.png']), ('share/appdata', ['data/mailnag.appdata.xml']), ('share/applications', [os.path.join(BUILD_PATCH_DIR, 'mailnag.desktop'), os.path.join(BUILD_PATCH_DIR, 'mailnag-config.desktop')])], cmdclass={'build': BuildData, 'install_data': InstallData, 'uninstall': Uninstall} )