pax_global_header00006660000000000000000000000064140122677220014515gustar00rootroot0000000000000052 comment=4a65218a61b0f34c9758a0ee7b33c6f54b68990a mailnag-2.2.0/000077500000000000000000000000001401226772200131265ustar00rootroot00000000000000mailnag-2.2.0/.gitignore000066400000000000000000000001321401226772200151120ustar00rootroot00000000000000*.pyc locale Mailnag/plugins/goaplugin.py Mailnag/plugins/messagingmenuplugin.py /.cache mailnag-2.2.0/AUTHORS000066400000000000000000000033341401226772200142010ustar00rootroot00000000000000Maintainer: =========== 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 Andreas Angerer Balló György Dan Christensen Denis Anuschewski Edwin Smulders Freeroot Hasan Yavuz Özderya Heiko Adams James Powell Leighton Earl Matthias Mailänder Oleg razer Taylor Braun-Jones Thorsten Leemhuis Thomas Haider Timo Kankare 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-2.2.0/LICENSE000066400000000000000000000432541401226772200141430ustar00rootroot00000000000000 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-2.2.0/Mailnag/000077500000000000000000000000001401226772200144765ustar00rootroot00000000000000mailnag-2.2.0/Mailnag/__init__.py000066400000000000000000000013701401226772200166100ustar00rootroot00000000000000# 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-2.2.0/Mailnag/backends/000077500000000000000000000000001401226772200162505ustar00rootroot00000000000000mailnag-2.2.0/Mailnag/backends/__init__.py000066400000000000000000000065421401226772200203700ustar00rootroot00000000000000# Copyright 2016 Timo Kankare # # 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. # """Backends to implement mail box specific functionality, like IMAP and POP3.""" from collections import namedtuple import json import re from Mailnag.backends.imap import IMAPMailboxBackend from Mailnag.backends.pop3 import POP3MailboxBackend from Mailnag.backends.local import MBoxBackend, MaildirBackend from Mailnag.common.utils import splitstr def _str_to_folders(folders_str): if re.match(r'^\[.*\]$', folders_str): folders = json.loads(folders_str) else: folders = splitstr(folders_str, ',') return folders def _folders_to_str(folders): return json.dumps(folders, ensure_ascii=False) def _str_to_bool(string): return bool(int(string)) def _bool_to_str(b): return str(int(b)) Param = namedtuple('Param', ['param_name', 'option_name', 'from_str', 'to_str', 'default_value'] ) Backend = namedtuple('Backend', ['backend_class', 'params']) _backends = { 'imap' : Backend(IMAPMailboxBackend, [ Param('user', 'user', str, str, ''), Param('password', 'password', str, str, ''), Param('server', 'server', str, str, ''), Param('port', 'port', str, str, ''), Param('ssl', 'ssl', _str_to_bool, _bool_to_str, True), Param('imap', 'imap', _str_to_bool, _bool_to_str, True), Param('idle', 'idle', _str_to_bool, _bool_to_str, True), Param('folders', 'folder', _str_to_folders, _folders_to_str, []), ]), 'pop3' : Backend(POP3MailboxBackend, [ Param('user', 'user', str, str, ''), Param('password', 'password', str, str, ''), Param('server', 'server', str, str, ''), Param('port', 'port', str, str, ''), Param('ssl', 'ssl', _str_to_bool, _bool_to_str, True), Param('imap', 'imap', _str_to_bool, _bool_to_str, False), Param('idle', 'idle', _str_to_bool, _bool_to_str, False), ]), 'mbox' : Backend(MBoxBackend, [ Param('path', 'path', str, str, ''), ]), 'maildir' : Backend(MaildirBackend, [ Param('path', 'path', str, str, ''), Param('folders', 'folder', _str_to_folders, _folders_to_str, []), ]), } def create_backend(mailbox_type, **kw): """Create mailbox backend of specified type and parameters.""" return _backends[mailbox_type].backend_class(**kw) def get_mailbox_parameter_specs(mailbox_type): """Returns mailbox backend specific parameter specification. The specification is a list objects which have atributes: * param_name - the name of argument in which parameter value is given to backend constructor * option_name - the name used in configuration * from_str - the function to convert string to parameter specific type * default_value - the value used when the option is not found """ return _backends[mailbox_type].params mailnag-2.2.0/Mailnag/backends/base.py000066400000000000000000000054321401226772200175400ustar00rootroot00000000000000# Copyright 2020 Patrick Ulbrich # Copyright 2016 Timo Kankare # # 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. # """Interface and base implementation for mailbox backends.""" from abc import ABCMeta, abstractmethod class MailboxBackend(object, metaclass=ABCMeta): """Interface for mailbox backends. Mailbox backend implements access to the specific type of mailbox. """ def __init__(self, **kw): """Constructor should accept any kind of backend specific parameters. """ @abstractmethod def open(self): """Opens the mailbox.""" raise NotImplementedError @abstractmethod def close(self): """Closes the mailbox.""" raise NotImplementedError @abstractmethod def is_open(self): """Returns true if mailbox is open.""" raise NotImplementedError @abstractmethod def list_messages(self): """Lists unseen messages from the mailbox for this account. Yields tuples (folder, message, flags) for every message. """ raise NotImplementedError @abstractmethod def request_folders(self): """Returns list of folder names available in the mailbox. Raises an exceptions if mailbox does not support folders. """ raise NotImplementedError def supports_mark_as_seen(self): """Returns True if mailbox supports flagging mails as seen.""" # Default implementation return False @abstractmethod def mark_as_seen(self, mails): """Asks mailbox to flag mails in the list as seen. This may raise an exception if mailbox does not support this action. """ raise NotImplementedError def supports_notifications(self): """Returns True if mailbox supports notifications.""" # Default implementation return False @abstractmethod def notify_next_change(self, callback=None, timeout=None): """Asks mailbox to notify next change. Callback is called when new mail arrives or a mail is removed. This may raise an exception if mailbox does not support notifications. """ raise NotImplementedError @abstractmethod def cancel_notifications(self): """Cancels notifications. This may raise an exception if mailbox does not support notifications. """ raise NotImplementedError mailnag-2.2.0/Mailnag/backends/imap.py000066400000000000000000000160431401226772200175540ustar00rootroot00000000000000# Copyright 2011 - 2021 Patrick Ulbrich # Copyright 2020 Andreas Angerer # Copyright 2016 Timo Kankare # Copyright 2016 Thomas Haider # Copyright 2011 Ralf Hersel # Copyright 2019 razer # # 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. # """Implementation for IMAP mailbox connection.""" import email import logging import re from Mailnag.backends.base import MailboxBackend import Mailnag.common.imaplib2 as imaplib from Mailnag.common.imaplib2 import AUTH from Mailnag.common.exceptions import InvalidOperationException from Mailnag.common.mutf7 import encode_mutf7, decode_mutf7 class IMAPMailboxBackend(MailboxBackend): """Implementation of IMAP mail boxes.""" def __init__(self, name = '', user = '', password = '', oauth2string = '', server = '', port = '', ssl = True, folders = [], idle=True, **kw): self.name = name self.user = user self.password = password self.oauth2string = oauth2string self.server = server self.port = port self.ssl = ssl # bool self.folders = [encode_mutf7(folder) for folder in folders] self.idle = idle self._conn = None def open(self): if self._conn != None: raise InvalidOperationException("Account is aready open") self._conn = self._connect() def close(self): # if conn has already been closed, don't try to close it again if self._conn != None: conn = self._conn self._conn = None self._disconnect(conn) def is_open(self): return self._conn != None def list_messages(self): self._ensure_open() conn = self._conn if len(self.folders) == 0: folder_list = [ 'INBOX' ] else: folder_list = self.folders for folder in folder_list: # select IMAP folder conn.select(f'"{folder}"', readonly = True) try: status, data = conn.uid('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.uid('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_bytes(response_part[1]) except: logging.debug("Couldn't get IMAP message.") continue yield (folder, msg, { 'uid' : num.decode("utf-8"), 'folder' : folder }) def request_folders(self): lst = [] # Always create a new connection as an existing one may # be used for IMAP IDLE. conn = self._connect() try: status, data = conn.list() finally: self._disconnect(conn) for d in data: match = re.match(r'.+\s+("."|"?NIL"?)\s+"?([^"]+)"?$', d.decode('utf-8')) if match == None: logging.warning("Folder format not supported.") else: folder = match.group(2) lst.append(decode_mutf7(folder)) return lst def supports_mark_as_seen(self): return True def mark_as_seen(self, mails): # Always create a new connection as an existing one may # be used for IMAP IDLE. conn = self._connect() try: sorted_mails = sorted(mails, key = lambda m : m.flags['folder'] if 'folder' in m.flags else '') last_folder = '' for m in sorted_mails: if ('uid' in m.flags) and ('folder' in m.flags): try: folder = m.flags['folder'] if folder != last_folder: conn.select(f'"{folder}"', readonly = False) last_folder = folder status, data = conn.uid("STORE", m.flags['uid'], "+FLAGS", "(\Seen)") except: logging.warning("Failed to set mail with uid %s to seen on server (account: '%s').", m.flags['uid'], acc.name) finally: self._disconnect(conn) def supports_notifications(self): """Returns True if mailbox supports notifications. IMAP mailbox supports notifications if idle parameter is True""" return self.idle def notify_next_change(self, callback=None, timeout=None): self._ensure_open() # 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. # idle callback (runs on a further thread) def _idle_callback(args): # error = (error_type, error_val) response, callback_arg, error = args # call actual callback callback(error) self._conn.idle(callback = _idle_callback, timeout = timeout) def cancel_notifications(self): # NOTE: Don't throw if the connection is closed. # Analogous to close(). # (Otherwise cleanup code like in Idler._idle() will fail) # self._ensure_open() try: if self._conn != None: # Exit possible active idle state. # (also calls idle_callback) self._conn.noop() except: pass def _connect(self): 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) elif 'AUTH=CRAM-MD5' in conn.capabilities: # use CRAM-MD5 auth if available conn.login_cram_md5(self.user, self.password) else: conn.login(self.user, self.password) except: try: if conn != None: # conn.close() # allowed in SELECTED state only conn.logout() except: pass raise # re-throw exception # notify_next_change() (IMAP IDLE) requires a selected folder if conn.state == AUTH: self._select_single_folder(conn) return conn def _disconnect(self, conn): try: conn.close() finally: # Closing the connection may fail (e.g. wrong state), # but resources need to be freed anyway. conn.logout() def _select_single_folder(self, conn): if len(self.folders) == 1: folder = self.folders[0] else: folder = "INBOX" conn.select(f'"{folder}"', readonly = True) def _ensure_open(self): if not self.is_open(): raise InvalidOperationException("Account is not open") mailnag-2.2.0/Mailnag/backends/local.py000066400000000000000000000101601401226772200177120ustar00rootroot00000000000000# Copyright 2020 Patrick Ulbrich # Copyright 2016 Timo Kankare # # 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. # """Implementation of local mailboxes, like mbox and maildir.""" import email import mailbox import logging import os.path from Mailnag.backends.base import MailboxBackend class MBoxBackend(MailboxBackend): """Implementation of mbox mail boxes.""" def __init__(self, name = '', path=None, **kw): """Initialize mbox mailbox backend with a name and path.""" self._name = name self._path = path self._opened = False def open(self): """'Open' mbox. (Actually just checks that mailbox file exists.)""" if not os.path.isfile(self._path): raise IOError('Mailbox {} does not exist.'.format(self._path)) self._opened = True def close(self): """Close mbox.""" self._opened = False def is_open(self): """Return True if mailbox is opened.""" return self._opened def list_messages(self): """List unread messages from the mailbox. Yields pairs (folder, message) where folder is always ''. """ mbox = mailbox.mbox(self._path, create=False) folder = '' try: for msg in mbox: if 'R' not in msg.get_flags(): yield (folder, msg, {}) finally: mbox.close() def request_folders(self): """mbox does not suppoert folders.""" raise NotImplementedError("mbox does not support folders") def supports_mark_as_seen(self): return False def mark_as_seen(self, mails): # TODO: local mailboxes should support this raise NotImplementedError def notify_next_change(self, callback=None, timeout=None): raise NotImplementedError("mbox does not support notifications") def cancel_notifications(self): raise NotImplementedError("mbox does not support notifications") class MaildirBackend(MailboxBackend): """Implementation of maildir mail boxes.""" def __init__(self, name = '', path=None, folders=[], **kw): """Initialize maildir mailbox backend with a name, path and folders.""" self._name = name self._path = path self._folders = folders self._opened = False def open(self): """'Open' mailbox. (Actually just checks that maildir directory exists.)""" if not os.path.isdir(self._path): raise IOError('Mailbox {} does not exist.'.format(self._path)) self._opened = True def close(self): """Close mailbox.""" self._opened = False def is_open(self): """Return True if mailbox is opened.""" return self._opened def list_messages(self): """List unread messages from the mailbox. Yields pairs (folder, message). """ folders = self._folders if len(self._folders) != 0 else [''] root_maildir = mailbox.Maildir(self._path, factory=None, create=False) try: for folder in folders: maildir = self._get_folder(root_maildir, folder) for msg in maildir: if 'S' not in msg.get_flags(): yield folder, msg finally: root_maildir.close() def request_folders(self): """Lists folders from maildir.""" maildir = mailbox.Maildir(self._path, factory=None, create=False) try: return [''] + maildir.list_folders() finally: maildir.close() def notify_next_change(self, callback=None, timeout=None): raise NotImplementedError("maildir does not support notifications") def cancel_notifications(self): raise NotImplementedError("maildir does not support notifications") def _get_folder(self, maildir, folder): """Returns folder instance of the given maildir.""" if folder == '': return maildir else: return maildir.get_folder(folder) mailnag-2.2.0/Mailnag/backends/pop3.py000066400000000000000000000072071401226772200175110ustar00rootroot00000000000000# Copyright 2011 - 2019 Patrick Ulbrich # Copyright 2016 Timo Kankare # Copyright 2016 Thomas Haider # Copyright 2011 Ralf Hersel # Copyright 2019 razer # # 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. # """Implementation for POP3 mailbox connection.""" import email import logging import poplib from Mailnag.backends.base import MailboxBackend from Mailnag.common.exceptions import InvalidOperationException class POP3MailboxBackend(MailboxBackend): """Implementation of POP3 mail boxes.""" def __init__(self, name = '', user = '', password = '', oauth2string = '', server = '', port = '', ssl = True, **kw): self.name = name self.user = user self.password = password self.oauth2string = oauth2string self.server = server self.port = port self.ssl = ssl # bool self._conn = None def open(self): if self._conn != None: raise InvalidOperationException("Account is aready open") 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)) try: conn.stls() except: logging.warning("Using unencrypted connection for account '%s'" % self.name) conn.getwelcome() conn.user(self.user) conn.pass_(self.password) except: try: if conn != None: conn.quit() except: pass raise # re-throw exception self._conn = conn def close(self): if self._conn != None: self._conn.quit() self._conn = None def is_open(self): return (self._conn != None) and \ ('sock' in self._conn.__dict__) def list_messages(self): self._ensure_open() conn = self._conn folder = '' # 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 byte sequence message_bytes = b'\n'.join(message) try: # put message into email object and make a dictionary msg = dict(email.message_from_bytes(message_bytes)) except: logging.debug("Couldn't get msg from POP message.") continue yield (folder, msg, {}) def request_folders(self): raise NotImplementedError("POP3 does not support folders") def supports_mark_as_seen(self): return False def mark_as_seen(self, mails): raise NotImplementedError def notify_next_change(self, callback=None, timeout=None): raise NotImplementedError("POP3 does not support notifications") def cancel_notifications(self): raise NotImplementedError("POP3 does not support notifications") def _ensure_open(self): if not self.is_open(): raise InvalidOperationException("Account is not open") mailnag-2.2.0/Mailnag/common/000077500000000000000000000000001401226772200157665ustar00rootroot00000000000000mailnag-2.2.0/Mailnag/common/__init__.py000066400000000000000000000013701401226772200201000ustar00rootroot00000000000000# 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-2.2.0/Mailnag/common/accounts.py000066400000000000000000000236031401226772200201630ustar00rootroot00000000000000# Copyright 2011 - 2020 Patrick Ulbrich # Copyright 2016 Thomas Haider # Copyright 2016, 2018 Timo Kankare # 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 logging import hashlib from Mailnag.backends import create_backend, get_mailbox_parameter_specs from Mailnag.common.secretstore import SecretStore from Mailnag.common.dist_cfg import PACKAGE_NAME account_defaults = { 'enabled' : '0', 'type' : 'imap', 'name' : '', 'user' : '', 'password' : '', 'server' : '', 'port' : '', 'ssl' : '1', 'imap' : '1', 'idle' : '1', 'folder' : '[]' } # # Account class # class Account: def __init__(self, mailbox_type = None, enabled = False, name = '', **kw): self._backend = None self.set_config( mailbox_type=mailbox_type, name=name, enabled=enabled, config=kw) def set_config(self, mailbox_type, enabled, name, config): """Set accounts configuration.""" self.enabled = enabled if mailbox_type: self.mailbox_type = mailbox_type elif 'imap' in config: self.mailbox_type = 'imap' if config.get('imap', True) else 'pop3' else: self.mailbox_type = '' self.name = name self.user = config.get('user', '') self.password = config.get('password', '') self.oauth2string = config.get('oauth2string', '') self.server = config.get('server', '') self.port = config.get('port', '') self.ssl = config.get('ssl', True) self.imap = config.get('imap', True) self.idle = config.get('idle', False) self.folders = config.get('folders', []) self._rest_of_config = config if self._backend and self._backend.is_open(): self._backend.close() self._backend = None def get_config(self): """Return account's configuration as a dict.""" config = { 'enabled': self.enabled, 'mailbox_type': self.mailbox_type, 'name': self.name, } config.update(self._get_backend_config()) return config def open(self): """Open mailbox for the account.""" self._get_backend().open() def close(self): """Close mailbox for this account.""" self._get_backend().close() # Indicates whether the account # holds an active existing connection. def is_open(self): """Returns true if the mailbox is opened.""" return self._get_backend().is_open() def list_messages(self): """Lists unseen messages from the mailbox for this account. Yields a set of tuples (folder, message). """ return self._get_backend().list_messages() def supports_notifications(self): """Returns True if account supports notifications.""" return self._get_backend().supports_notifications() def notify_next_change(self, callback=None, timeout=None): """Asks mailbox to notify next change. Callback is called when new mail arrives or removed. This may raise an exception if mailbox does not support notifications. """ self._get_backend().notify_next_change(callback, timeout) def cancel_notifications(self): """Cancels notifications. This may raise an exception if mailbox does not support notifications. """ self._get_backend().cancel_notifications() def request_server_folders(self): """Requests folder names (list) from a server. Returns an empty list if mailbox does not support folders. """ return self._get_backend().request_folders() def supports_mark_as_seen(self): return self._get_backend().supports_mark_as_seen() def mark_as_seen(self, mails): self._get_backend().mark_as_seen(mails) def get_id(self): """Returns unique id for the account.""" # Assumption: The name of the account is unique. return str(hash(self.name)) def _get_backend(self): if not self._backend: backend_config = self._get_backend_config() self._backend = create_backend(self.mailbox_type, name=self.name, **backend_config) return self._backend def _get_backend_config(self): config = {} imap_pop_config = { 'user': self.user, 'password': self.password, 'oauth2string': self.oauth2string, 'server': self.server, 'port': self.port, 'ssl': self.ssl, 'imap': self.imap, 'idle': self.idle, 'folders': self.folders, } config.update(imap_pop_config) config.update(self._rest_of_config) return config # # AccountManager class # class AccountManager: def __init__(self): self._accounts = [] self._removed = [] self._secretstore = SecretStore.get_default() if self._secretstore == None: logging.warning("Failed to create secretstore - account passwords will be stored in plaintext config file.") 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): if cfg.has_option(section_name, 'type'): mailbox_type = self._get_account_cfg(cfg, section_name, 'type') imap = (mailbox_type == 'imap') else: imap = bool(int(self._get_account_cfg(cfg, section_name, 'imap'))) mailbox_type = 'imap' if imap else 'pop3' name = self._get_account_cfg(cfg, section_name, 'name') option_spec = get_mailbox_parameter_specs(mailbox_type) options = self._get_cfg_options(cfg, section_name, option_spec) # TODO: Getting a password from the secretstore is mailbox specific. # Not every backend requires a password. user = options.get('user') server = options.get('server') if self._secretstore != None and user and server: password = self._secretstore.get(self._get_account_id(user, server, imap)) if not password: password = '' options['password'] = password acc = Account(enabled=enabled, name=name, mailbox_type=mailbox_type, **options) 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 secretstore # (it's important to do this before adding accounts, # in case multiple accounts with the same id exist). if self._secretstore != None: for acc in self._removed: self._secretstore.remove(self._get_account_id(acc.user, acc.server, acc.imap)) 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, 'type', acc.mailbox_type) cfg.set(section_name, 'name', acc.name) config = acc.get_config() option_spec = get_mailbox_parameter_specs(acc.mailbox_type) # TODO: Storing a password is mailbox specific. # Not every backend requires a password. if self._secretstore != None: self._secretstore.set(self._get_account_id(acc.user, acc.server, acc.imap), acc.password, f'{PACKAGE_NAME.capitalize()} password for account {acc.user}@{acc.server}') config['password'] = '' self._set_cfg_options(cfg, section_name, config, option_spec) i = i + 1 def _get_account_id(self, user, server, is_imap): # TODO : Introduce account.uuid when rewriting account and backend code return hashlib.md5((user + server + str(is_imap)).encode('utf-8')).hexdigest() 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] def _get_cfg_options(self, cfg, section_name, option_spec): options = {} for s in option_spec: options[s.param_name] = self._get_cfg_option(cfg, section_name, s.option_name, s.from_str, s.default_value) return options def _get_cfg_option(self, cfg, section_name, option_name, convert, default_value): if convert and cfg.has_option(section_name, option_name): value = convert(cfg.get(section_name, option_name)) else: value = default_value return value def _set_cfg_options(self, cfg, section_name, options, option_spec): for s in option_spec: if s.to_str and s.param_name in options: value = s.to_str(options[s.param_name]) else: value = s.default_value cfg.set(section_name, s.option_name, value) mailnag-2.2.0/Mailnag/common/config.py000066400000000000000000000030321401226772200176030ustar00rootroot00000000000000# Copyright 2011 - 2021 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', 'mailbox_seen_flags' : '1', 'autostart' : '1', 'connectivity_test' : 'networkmonitor', '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.read_dict(mailnag_defaults) 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, 'w') as configfile: cfg.write(configfile) mailnag-2.2.0/Mailnag/common/dist_cfg.py000066400000000000000000000041071401226772200201240ustar00rootroot00000000000000# Copyright 2012 - 2020 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 = '2.2.0' # 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-2.2.0/Mailnag/common/exceptions.py000066400000000000000000000015471401226772200205300ustar00rootroot00000000000000# 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-2.2.0/Mailnag/common/i18n.py000066400000000000000000000021061401226772200171160ustar00rootroot00000000000000# 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).gettext mailnag-2.2.0/Mailnag/common/imaplib2.py000066400000000000000000002650331401226772200200500ustar00rootroot00000000000000#!/usr/bin/env python3 """Threaded IMAP4 client for Python 3. 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", "Mon2num", "MonthNames", "InternalDate") __version__ = "3.05" __release__ = "3" __revision__ = "05" __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. Support for specifying SSL version by Ryan Kavanagh July 2013. Fix for gmail "read 0" error provided by Jim Greenleaf August 2013. Fix for offlineimap "indexerror: string index out of range" bug provided by Eygene Ryabinkin August 2013. Fix for missing idle_lock in _handler() provided by Franklin Brook August 2014. Conversion to Python3 provided by F. Malina February 2015. Fix for READ-ONLY error from multiple EXAMINE/SELECT calls by Pierre-Louis Bonicoli March 2015. Fix for null strings appended to untagged responses by Pierre-Louis Bonicoli March 2015. Fix for correct byte encoding for _CRAM_MD5_AUTH taken from python3.5 imaplib.py June 2015. Fix for correct Python 3 exception handling by Tobias Brink August 2015. Fix to allow interruptible IDLE command by Tim Peoples September 2015. Add support for TLS levels by Ben Boeckel September 2015. Fix for shutown exception by Sebastien Gross November 2015.""" __author__ = "Piers Lauder " __URL__ = "http://imaplib2.sourceforge.net" __license__ = "Python License" import binascii, calendar, errno, os, queue, random, re, select, socket, sys, time, threading, zlib select_module = select # Globals CRLF = b'\r\n' IMAP4_PORT = 143 IMAP4_SSL_PORT = 993 IDLE_TIMEOUT_RESPONSE = b'* 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 TLS_SECURE = "tls_secure" # Recognised TLS levels TLS_NO_SSL = "tls_no_ssl" TLS_COMPAT = "tls_compat" 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), 'ENABLE': ((AUTH,), False), '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, LOGOUT, 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 bytes using characters from the set 'A'..'P' """ val = b''; AP = b'ABCDEFGHIJKLMNOP' num = int(abs(num)) while num: num, mod = divmod(num, 16) val = AP[mod:mod+1] + val return 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 = parent.tagpre + bytes(str(parent.tagnum), 'ASCII') 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(threading.TIMEOUT_MAX) 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) string arguments to commands are converted to bytes, except for AUTHENTICATE, and the last argument to APPEND which is passed as an IMAP4 literal. NB: the 'password' argument to the LOGIN command is always quoted. 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 # These must be encoded according to utf8 setting in _mode_xxx(): _literal = br'.*{(?P\d+)}$' _untagged_status = br'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?' continuation_cre = re.compile(br'\+( (?P.*))?') mapCRLF_cre = re.compile(br'\r\n|\r|\n') response_code_cre = re.compile(br'\[(?P[A-Z-]+)( (?P[^\]]*))?\]') untagged_response_cre = re.compile(br'\* (?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.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 = False # Expecting message data self._expecting_data_len = 0 # How many characters we expect 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 self._tls_established = False # 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(br'(?P' + self.tagpre + br'\d+) (?P[A-Z]+) (?P.*)', re.ASCII) self._mode_ascii() 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(name='welcome', 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' % repr(self.welcome)) self._get_capabilities() 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) def __enter__(self): return self def __exit__(self, *args): try: self.logout() except OSError: pass def _mode_ascii(self): self.utf8_enabled = False self._encoding = 'ascii' self.literal_cre = re.compile(self._literal, re.ASCII) self.untagged_status_cre = re.compile(self._untagged_status, re.ASCII) def _mode_utf8(self): self.utf8_enabled = True self._encoding = 'utf-8' self.literal_cre = re.compile(self._literal) self.untagged_status_cre = re.compile(self._untagged_status) # 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.""" return socket.create_connection((self.host, self.port)) def ssl_wrap_socket(self): try: import ssl TLS_MAP = {} if hasattr(ssl, "PROTOCOL_TLSv1_2"): TLS_MAP[TLS_SECURE] = { "tls1_2": ssl.PROTOCOL_TLSv1_2, "tls1_1": ssl.PROTOCOL_TLSv1_1, } else: TLS_MAP[TLS_SECURE] = {} TLS_MAP[TLS_NO_SSL] = TLS_MAP[TLS_SECURE].copy() TLS_MAP[TLS_NO_SSL].update({ "tls1": ssl.PROTOCOL_TLSv1, }) TLS_MAP[TLS_COMPAT] = TLS_MAP[TLS_NO_SSL].copy() TLS_MAP[TLS_COMPAT].update({ "ssl23": ssl.PROTOCOL_SSLv23, None: ssl.PROTOCOL_SSLv23, }) if hasattr(ssl, "PROTOCOL_SSLv3"): # Might not be available. TLS_MAP[TLS_COMPAT].update({ "ssl3": ssl.PROTOCOL_SSLv3 }) if self.ca_certs is not None: cert_reqs = ssl.CERT_REQUIRED else: cert_reqs = ssl.CERT_NONE if self.tls_level not in TLS_MAP: raise RuntimeError("unknown tls_level: %s" % self.tls_level) if self.ssl_version not in TLS_MAP[self.tls_level]: raise socket.sslerror("Invalid SSL version '%s' requested for tls_version '%s'" % (self.ssl_version, self.tls_level)) ssl_version = TLS_MAP[self.tls_level][self.ssl_version] self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ca_certs=self.ca_certs, cert_reqs=cert_reqs, ssl_version=ssl_version) ssl_exc = ssl.SSLError self.read_fd = self.sock.fileno() except ImportError: # No ssl module, and socket.ssl has no fileno(), and does not allow certificate verification raise socket.sslerror("imaplib SSL mode does not work without ssl module") 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) # 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) 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(READ_SIZE) 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".""" try: self.sock.shutdown(socket.SHUT_RDWR) except Exception as e: # The server might already have closed the connection if e.errno != errno.ENOTCONN: raise finally: 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 if isinstance(message, str): message = bytes(message, 'ASCII') literal = self.mapCRLF_cre.sub(CRLF, message) if self.utf8_enabled: literal = b'UTF8 (' + literal + b')' self.literal = literal 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, the 'response' argument will be a 'bytes'. It should return bytes 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 enable(self, capability): """Send an RFC5161 enable string to the server. (typ, [data]) = .enable(capability) """ if 'ENABLE' not in self.capabilities: raise self.error("Server does not support ENABLE") typ, data = self._simple_command('ENABLE', capability) if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper(): self._mode_utf8() return typ, data def examine(self, mailbox='INBOX', **kw): """(typ, [data]) = examine(mailbox='INBOX') 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) 'kv_pairs' is a possibly empty list of keys and values. 'data' is a list of ID key value pairs or NIL. NB: a single argument is assumed to be correctly formatted and is passed through unchanged (for backward compatibility with earlier version). Exchange information for problem analysis and determination. The ID extension is defined in RFC 2971. """ name = 'ID' kw['untagged_response'] = name if not kv_pairs: data = 'NIL' elif len(kv_pairs) == 1: data = kv_pairs[0] # Assume invoker passing correctly formatted string (back-compat) else: data = '(%s)' % ' '.join([(arg and self._quote(arg) or 'NIL') for arg in kv_pairs]) return self._simple_command(name, data, **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 pwd = (self.password.encode('utf-8') if isinstance(self.password, str) else self.password) return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').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. If UTF8 is enabled, charset MUST be None. 'data' is space separated list of matching message numbers.""" name = 'SEARCH' kw['untagged_response'] = name if charset: if self.utf8_enabled: raise self.error("Non-None charset not valid in UTF8 mode") 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. (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.""" self.mailbox = mailbox self.is_readonly = bool(readonly) 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, ssl_version="ssl23", tls_level=TLS_COMPAT, **kw): """(typ, [data]) = starttls(keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", tls_level="tls_compat") Start TLS negotiation as per RFC 2595.""" name = 'STARTTLS' if name not in self.capabilities: raise self.abort('TLS not supported by server') if 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 self.ssl_version = ssl_version self.tls_level = tls_level try: self.ssl_wrap_socket() finally: # Restart reader thread self.rdth = threading.Thread(target=self._reader) self.rdth.setDaemon(True) self.rdth.start() self._get_capabilities() 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 = b'' 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 += ["%.80r"]' % (typ, len(urd)-1, dat)) def _check_bye(self): bye = self._get_untagged_response('BYE', leave=True) if bye: raise self.abort(bye[-1].decode('ASCII', 'replace')) def _choose_nonull_or_dflt(self, dflt, *args): if isinstance(dflt, str): dflttyp = str # Allow any string type else: dflttyp = type(dflt) for arg in args: if arg is not None: if isinstance(arg, dflttyp): return arg if __debug__: self._log(0, 'bad arg 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(threading.TIMEOUT_MAX) 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() if name in ('EXAMINE', 'SELECT'): self.commands_lock.acquire() self.untagged_responses = [] # Flush all untagged responses self.commands_lock.release() else: for typ in ('OK', 'NO', 'BAD'): while self._get_untagged_response(typ): continue if not self.is_readonly and self._get_untagged_response('READ-ONLY', leave=True): 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) name = bytes(name, self._encoding) data = rqb.tag + b' ' + name for arg in args: if arg is None: continue if isinstance(arg, str): arg = bytes(arg, self._encoding) data = data + b' ' + arg literal = self.literal if literal is not None: self.literal = None if type(literal) is type(self._command): literator = literal else: literator = None data = data + bytes(' {%s}' % len(literal), self._encoding) if __debug__: self._log(4, 'data=%r' % data) rqb.data = data + CRLF if literal is None: self.ouq.put(rqb) return rqb # Must setup continuation expectancy *before* ouq.put crqb = self._request_push(name=name, 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, %r' % (ok, data)) # NO/BAD response? if not ok: break if data == 'go ahead': # Apparently not uncommon broken IMAP4 server response to AUTHENTICATE command data = '' # 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(name=name, tag='continuation') if __debug__: self._log(4, 'write literal size %s' % len(literal)) crqb.data = literal + CRLF self.ouq.put(crqb) if literator is None: break return rqb def _command_complete(self, rqb, kw): # Called for non-callback commands self._check_bye() typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) 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, cb_arg_list): # Called for callback commands response, cb_arg, error = cb_arg_list 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].decode('ASCII', 'replace')) 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 __debug__: self._log(4, '_command_completer(%s, %s, None) = %s' % (response, cb_arg, rqb.tag)) 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 = bytes('DONE', 'ASCII') + CRLF self.ouq.put(irqb) if __debug__: self._log(2, 'server IDLE finished') def _get_capabilities(self): typ, dat = self.capability() if dat == [None]: raise self.error('no CAPABILITY response from server') dat = str(dat[-1], "ASCII") dat = dat.upper() self.capabilities = tuple(dat.split()) 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) => %.80r' % (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: rlen = len(resp) dlen = min(self._expecting_data_len, rlen) if __debug__: self._log(5, '_put_response expecting data len %s, got %s' % (self._expecting_data_len, rlen)) self._expecting_data_len -= dlen self._expecting_data = (self._expecting_data_len != 0) 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, b''.join(self._accumulated_data))) self._accumulated_data = [] # Protocol mandates all lines terminated by CRLF resp = resp[:-2] if __debug__: self._log(5, '_put_response(%r)' % resp) 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 = True self._expecting_data_len = int(self.mo.group('size')) if __debug__: self._log(4, 'expecting literal size %s' % self._expecting_data_len) return typ = self._literal_expected[0] self._literal_expected = None if dat: 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 = str(self.mo.group('type'), 'ASCII') dat = self.mo.group('data') if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): self._append_untagged(str(self.mo.group('type'), 'ASCII'), self.mo.group('data')) if not tag in self.tagged_commands: if __debug__: self._log(1, 'unexpected tagged response: %r' % 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: '%r'" % resp) return self._request_pop('continuation', (True, self.mo.group('data'))) return if __debug__: self._log(1, "unexpected response: '%r'" % resp) return typ = str(self.mo.group('type'), 'ASCII') dat = self.mo.group('data') if dat is None: dat = b'' # Null untagged response if dat2: dat = dat + b' ' + dat2 # Is there a literal to come? if self._match(self.literal_cre, dat): self._expecting_data = True self._expecting_data_len = int(self.mo.group('size')) if __debug__: self._log(4, 'read literal size %s' % self._expecting_data_len) self._literal_expected = [typ, dat] return self._append_untagged(typ, dat) if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): self._append_untagged(str(self.mo.group('type'), 'ASCII'), self.mo.group('data')) if typ != 'OK': # NO, BYE, IDLE self._end_idle() # 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: %r' % (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: need_event = True else: need_event = False self.commands_lock.release() if __debug__: self._log(4, '_request_pop(%s, %r) [%d] = %s' % (name, data, len(self.tagged_commands), rqb.tag)) rqb.deliver(data) if need_event: if __debug__: self._log(3, 'state_change_free.set') self.state_change_free.set() 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, repr(kw), rqb.tag)) return rqb def _simple_command(self, name, *args, **kw): if 'callback' in kw: # Note: old calling sequence for back-compat with python <2.6 self._command(name, callback=self._command_completer, cb_arg=kw, cb_self=True, *args) 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) => %.80r' % (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: self.idle_lock.acquire() 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 self.idle_lock.release() try: 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, bytes): 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' % repr(val)) while not self.ouq.empty(): try: qel = self.ouq.get_nowait() if qel is not None: qel.abort(typ, val) except queue.Empty: break self.ouq.put(None) self.commands_lock.acquire() for name in list(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 = b'' 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 = 10 else: timeout = read_poll_timeout try: r = poll.poll(timeout) if __debug__: self._log(5, 'poll => %s' % repr(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) continue # Try again rxzero = 0 while True: stop = data.find(b'\n', start) if stop < 0: line_part += data[start:] break stop += 1 line_part, start, line = \ b'', stop, line_part + data[start:stop] if __debug__: self._log(4, '< %r' % 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 = b'' 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) continue # Try again rxzero = 0 while True: stop = data.find(b'\n', start) if stop < 0: line_part += data[start:] break stop += 1 line_part, start, line = \ b'', stop, (line_part + data[start:stop]).decode(errors='ignore') if __debug__: self._log(4, '< %r' % 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, '> %r' % 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_lock = threading.Lock() self.debug = self._choose_nonull_or_dflt(0, 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._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 # NB: bytes array if not l: return t = '\n\t\t' l = ['%s: "%s"' % (x[0], x[1][0] and b'" "'.join(x[1]) or '') for x in 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, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None, tls_level="tls_compat") 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); ssl_version - SSL version to use (default: "ssl23", choose from: "tls1","ssl3","ssl23"); 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. tls_level - TLS security level (default: "tls_compat"). The recognized values for tls_level are: tls_secure: accept only TLS protocols recognized as "secure" tls_no_ssl: disable SSLv2 and SSLv3 support tls_compat: accept all SSL/TLS versions 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, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None, tls_level=TLS_COMPAT): self.keyfile = keyfile self.certfile = certfile self.ca_certs = ca_certs self.cert_verify_cb = cert_verify_cb self.ssl_version = ssl_version self.tls_level = tls_level 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(READ_SIZE) 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: dlen = len(data) while dlen > 0: sent = self.sock.write(data) if sent == dlen: break # avoid copy data = data[sent:] dlen = dlen - sent def ssl(self): """ssl = ssl() Return 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 from io import DEFAULT_BUFFER_SIZE if __debug__: self._log(0, 'opening stream from command "%s"' % self.command) self._P = Popen(self.command, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True, bufsize=DEFAULT_BUFFER_SIZE) 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, READ_SIZE) 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() self._P.wait() 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 b'*' # 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 = b'' if isinstance(inp, str): inp = inp.encode('utf-8') while inp: if len(inp) > 48: t = inp[:48] inp = inp[48:] else: t = inp inp = b'' e = binascii.b2a_base64(t) if e: oup = oup + e[:-1] return oup def decode(self, inp): if not inp: return b'' 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 = {s.encode():n+1 for n, s in enumerate(MonthNames[1:])} InternalDate = re.compile(br'.*INTERNALDATE "' br'(?P[ 0123][0-9])-(?P[A-Z][a-z][a-z])-(?P[0-9][0-9][0-9][0-9])' br' (?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])' br' (?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])' br'"') def Internaldate2Time(resp): """time_tuple = Internaldate2Time(resp) Parse an IMAP4 INTERNALDATE string. Return corresponding local time. The return value is a time.struct_time instance or None if the string has wrong format.""" 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 == b'-': zone = -zone tt = (year, mon, day, hour, min, sec, -1, -1, -1) return time.localtime(calendar.timegm(tt) - 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. The date_time argument can be a number (int or float) representing seconds since epoch (as returned by time.time()), a 9-tuple representing local time, an instance of time.struct_time (as returned by time.localtime()), an aware datetime instance or a double-quoted string. In the last case, it is assumed to already be in the correct format.""" from datetime import datetime, timezone, timedelta if isinstance(date_time, (int, float)): tt = time.localtime(date_time) elif isinstance(date_time, tuple): try: gmtoff = date_time.tm_gmtoff except AttributeError: if time.daylight: dst = date_time[8] if dst == -1: dst = time.localtime(time.mktime(date_time))[8] gmtoff = -(time.timezone, time.altzone)[dst] else: gmtoff = -time.timezone delta = timedelta(seconds=gmtoff) dt = datetime(*date_time[:6], tzinfo=timezone(delta)) elif isinstance(date_time, datetime): if date_time.tzinfo is None: raise ValueError("date_time must be aware") dt = 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") fmt = '"%d-{}-%Y %H:%M:%S %z"'.format(MonthNames[dt.month]) return dt.strftime(fmt) FLAGS_cre = re.compile(br'.*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]' # # Option "-d " turns on debugging (use "-d 5" for everything) # Option "-i" tests that IDLE is interruptible # Option "-p " allows alternate ports if not __debug__: raise ValueError('Please run without -O') import getopt, getpass try: optlist, args = getopt.getopt(sys.argv[1:], 'd:il:s:p:') except getopt.error as val: optlist, args = (), () debug, debug_buf_lvl, port, stream_command, keyfile, certfile, idle_intr = (None,)*7 for opt,val in optlist: if opt == '-d': debug = int(val) debug_buf_lvl = debug - 1 elif opt == '-i': idle_intr = 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', ('""', '""')), ('list', ('""', '"%"')), ('create', ('imaplib2_test0',)), ('rename', ('imaplib2_test0', 'imaplib2_test1')), ('CREATE', ('imaplib2_test2',)), ('append', ('imaplib2_test2', None, None, test_mesg)), ('list', ('""', '"imaplib2_test%"')), ('select', ('imaplib2_test2',)), ('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)), ('examine', ()), ('select', ()), ('fetch', ('1:*', '(FLAGS UID)')), ('examine', ()), ('select', ()), ('uid', ('SEARCH', 'SUBJECT', '"IMAP4 test"')), ('uid', ('SEARCH', 'ALL')), ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')), ('recent', ()), ) AsyncError, M = None, None def responder(cb_arg_list): response, cb_arg, error = cb_arg_list 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 %s' % repr(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, ssl_version="tls1", debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl, tls_level="tls_no_ssl") 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', ('""', '"imaplib2_test%"'), cb=False): mo = re.match(br'.*"([^"]+)"$', ml) if mo: path = mo.group(1) else: path = ml.split()[-1] run('delete', (path,)) if 'ID' in M.capabilities: run('id', ()) run('id', ("(name imaplib2)",)) run('id', ("version", __version__, "os", os.uname()[0])) 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,)) # Asynchronous, to test interruption of 'idle' by 'noop' time.sleep(1) run('noop', (), cb=False) run('append', (None, None, None, test_mesg), cb=False) num = run('search', (None, 'ALL'), cb=False)[0].split()[0] dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False) M._mesg('fetch %s => %s' % (num, repr(dat))) run('idle', (2,)) run('store', (num, '-FLAGS', '(\Seen)'), cb=False), dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False) M._mesg('fetch %s => %s' % (num, repr(dat))) run('uid', ('STORE', num, 'FLAGS', '(\Deleted)')) run('expunge', ()) if idle_intr: M._mesg('HIT CTRL-C to interrupt IDLE') try: run('idle', (99,), cb=False) # Synchronous, to test interruption of 'idle' by INTR except KeyboardInterrupt: M._mesg('Thanks!') M._mesg('') raise elif idle_intr: M._mesg('chosen server does not report IDLE capability') 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: if not idle_intr or M is None or not 'IDLE' in M.capabilities: print('Tests failed.') if not debug: print(''' If you would like to see debugging output, try: %s -d5 ''' % sys.argv[0]) raise mailnag-2.2.0/Mailnag/common/mutf7.py000066400000000000000000000043761401226772200174140ustar00rootroot00000000000000''' This awesome piece of code is intented to encode and decode modified UTF-7 (the one that is used for IMAP folder names) encode_mutf7(text) - to encode decode_mutf7(text) - to decode ''' __author__ = "https://github.com/cheshire-mouse" __license__ = "WTFPL v. 2" import base64 import re ascii_codes = set(range(0x20,0x7f)) def __get_ascii(text): pos = 0 for c in text: if ord(c) not in ascii_codes : break pos += 1 return text[:pos] def __remove_ascii(text): pos = 0 for c in text: if ord(c) not in ascii_codes : break pos += 1 return text[pos:] def __get_nonascii(text): pos = 0 for c in text: if ord(c) in ascii_codes : break pos += 1 return text[:pos] def __remove_nonascii(text): pos = 0 for c in text: if ord(c) in ascii_codes : break pos += 1 return text[pos:] def __encode_modified_utf7(text): #modified base64 - good old base64 without padding characters (=) result = base64.b64encode(text.encode('utf-16be')).decode('utf-8').rstrip('=') result = result.replace('/',',') result = '&' + result + '-' return result def encode_mutf7(text): result = "" text = text.replace('&','&-') while len(text) > 0: result += __get_ascii(text) text = __remove_ascii(text) if len(text) > 0: result += __encode_modified_utf7(__get_nonascii(text)) text = __remove_nonascii(text) return result def __decode_modified_utf7(text): if text == '&-': return '&' #remove leading & and trailing - text_mb64 = text[1:-1] text_b64 = text_mb64.replace(',','/') #back to normal base64 with padding while len(text_b64) % 4 != 0: text_b64 += '=' text_u16 = base64.b64decode(text_b64) result = text_u16.decode('utf-16be') return result def decode_mutf7(text): rxp = re.compile('&[^&-]*-') match = rxp.search(text) while ( match ): encoded_text = match.group(0) decoded_text = __decode_modified_utf7(encoded_text) text = rxp.sub(decoded_text,text, count=1) match = rxp.search(text) result = text return result # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 mailnag-2.2.0/Mailnag/common/plugins.py000066400000000000000000000156551401226772200200350ustar00rootroot00000000000000# Copyright 2013 - 2020 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 enum import Enum 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. # class HookTypes(Enum): # 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 : [] } # Priority should be an integer value fom 0 (very high) to 100 (very low) # Plugin hooks will be called in order from high to low priority. def register_hook_func(self, hooktype, func, priority = 100): self._hooks[hooktype].append( (priority, func) ) def unregister_hook_func(self, hooktype, func): pairs = self._hooks[hooktype] pair = next(pa for pa in pairs if (pa[1] == func)) pairs.remove(pair) def get_hook_funcs(self, hooktype): pairs_by_prio = sorted(self._hooks[hooktype], key = lambda p: p[0]) funcs = [ f for p, f in pairs_by_prio ] return funcs # 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). 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.items(): if k not in config: 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-2.2.0/Mailnag/common/secretstore.py000066400000000000000000000035331401226772200207060ustar00rootroot00000000000000# Copyright 2019 Patrick Ulbrich # Copyright 2019 razer # # 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. # from Mailnag.common.dist_cfg import PACKAGE_NAME try: import gi gi.require_version('Secret', '1') from gi.repository import Secret _libsecret_err = None except ModuleNotFoundError as e: _libsecret_err = e class SecretStore(): _instance = None def __init__(self): if _libsecret_err != None: raise _libsecret_err self._schema = Secret.Schema.new( f'com.github.pulb.{PACKAGE_NAME}', Secret.SchemaFlags.NONE, {'id' : Secret.SchemaAttributeType.STRING}) def get(self, secret_id): return Secret.password_lookup_sync(self._schema, {'id': secret_id}, None) def set(self, secret_id, secret, description): Secret.password_store_sync( self._schema, {'id': secret_id}, Secret.COLLECTION_DEFAULT, description, secret, None) def remove(self, secret_id): return Secret.password_clear_sync(self._schema, {'id': secret_id}, None) @staticmethod def get_default(): if _libsecret_err != None: return None if SecretStore._instance == None: SecretStore._instance = SecretStore() return SecretStore._instance mailnag-2.2.0/Mailnag/common/subproc.py000066400000000000000000000071651401226772200200260ustar00rootroot00000000000000# 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.items(): 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.items(): 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-2.2.0/Mailnag/common/utils.py000066400000000000000000000066501401226772200175070ustar00rootroot00000000000000# Copyright 2011 - 2019 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 logging.handlers import inspect from Mailnag.common.dist_cfg import PACKAGE_NAME, DBUS_BUS_NAME, DBUS_OBJ_PATH LOG_FORMAT = '%(levelname)s (%(asctime)s): %(message)s' LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' def init_logging(enable_stdout = True, enable_syslog = True, log_level = logging.DEBUG): logging.basicConfig( format = LOG_FORMAT, datefmt = LOG_DATE_FORMAT, level = log_level) logger = logging.getLogger('') if not enable_stdout: stdout_handler = logger.handlers[0] logger.removeHandler(stdout_handler) if enable_syslog: syslog_handler = logging.handlers.SysLogHandler(address='/dev/log') syslog_handler.setLevel(log_level) syslog_handler.setFormatter(logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)) logger.addHandler(syslog_handler) 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.encode('utf-8') 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(wait_for_completion = True): 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() if wait_for_completion: while bus.name_has_owner(DBUS_BUS_NAME): time.sleep(2) print('OK') except: print('FAILED') mailnag-2.2.0/Mailnag/configuration/000077500000000000000000000000001401226772200173455ustar00rootroot00000000000000mailnag-2.2.0/Mailnag/configuration/__init__.py000066400000000000000000000013701401226772200214570ustar00rootroot00000000000000# 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-2.2.0/Mailnag/configuration/accountdialog.py000066400000000000000000000451671401226772200225500ustar00rootroot00000000000000# Copyright 2011 - 2021 Patrick Ulbrich # Copyright 2016, 2019 Timo Kankare # # 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 gi gi.require_version('Gtk', '3.0') gi.require_version('GLib', '2.0') from gi.repository import GObject, GLib, Gtk, Gdk 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 IDX_MBOX = 6 IDX_MAILDIR = 7 GMAIL_SUPPORT_PAGE = "https://support.google.com/mail/answer/185833?hl=en" PROVIDER_CONFIGS = [ [ 'Gmail', 'imap.gmail.com', '993'], [ 'GMX', 'imap.gmx.net', '993'], [ 'Web.de', 'imap.web.de', '993'], [ 'Yahoo', 'imap.mail.yahoo.com', '993'] ] def folder_cell_data(tree_column, cell, model, iter, *data): value = model[iter][1] if value == '': value = '[root folder]' cell.set_property('text', value) 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_widget.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, \ "password_info_icon_released" : self._on_password_info_icon_released \ }) self._window = Gtk.Dialog(title = _('Mail Account'), parent = parent, use_header_bar = True, \ buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)) self._window.set_default_response(Gtk.ResponseType.OK) self._window.set_default_size(400, 0) self._box = self._window.get_content_area() self._box.set_border_width(12) self._box.set_spacing(12) self._box.pack_start(builder.get_object("account_widget"), True, True, 0) 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._label_account_user = builder.get_object("label_account_user") self._entry_account_user = builder.get_object("entry_account_user") self._label_account_password = builder.get_object("label_account_password") 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._label_account_file_path = builder.get_object("label_account_file_path") self._chooser_account_file_path = builder.get_object("chooser_file_path") self._label_account_directory_path = builder.get_object("label_account_directory_path") self._chooser_account_directory_path = builder.get_object("chooser_directory_path") 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_ok = self._window.get_widget_for_response(Gtk.ResponseType.OK) self._button_ok.set_sensitive(False) self._error_label = None self._folders_received = False self._selected_folder_count = 0 self._pwd_info_icon = self._entry_account_password.get_icon_name(Gtk.EntryIconPosition.SECONDARY) 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) column_folders_name.set_cell_data_func(renderer_folders_name, folder_cell_data) 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 == Gtk.ResponseType.OK: self._configure_account(self._acc) self._window.destroy() return res def get_account(self): return self._acc def _load_account(self, acc): config = acc.get_config() self._entry_account_name.set_text(acc.name) if 'user' in config: self._entry_account_user.set_text(config['user']) if 'password' in config: self._entry_account_password.set_text(config['password']) if 'server' in config: self._entry_account_server.set_text(config['server']) if 'port' in config: self._entry_account_port.set_text(config['port']) if 'idle' in config: self._chk_account_push.set_active(config['idle']) if 'folders' in config: self._chk_account_push.set_sensitive(len(config['folders']) < 2) if 'ssl' in config: self._chk_account_ssl.set_active(config['ssl']) if ('path' in config) and os.path.exists(config.get('path')): self._chooser_account_file_path.set_filename(config.get('path')) # Workaround: fire the on-entry-changed signal manually as it is only called # when the user changes the file by mouse or keyboard. # See https://developer.gnome.org/gtk3/stable/GtkFileChooserButton.html#GtkFileChooserButton-file-set self._on_entry_changed(self._chooser_account_file_path) self._chooser_account_directory_path.set_filename(config.get('path')) self._on_entry_changed(self._chooser_account_directory_path) def _configure_account(self, acc): config = {} acctype = self._cmb_account_type.get_active() if (acctype == IDX_POP3) or (acctype == IDX_IMAP): name = self._entry_account_name.get_text() config['user'] = self._entry_account_user.get_text() config['password'] = self._entry_account_password.get_text() config['server'] = self._entry_account_server.get_text() config['folders'] = [] config['port'] = self._entry_account_port.get_text() config['ssl'] = self._chk_account_ssl.get_active() if acctype == IDX_POP3: mailbox_type = 'pop3' config['imap'] = False config['idle'] = False elif acctype == IDX_IMAP: mailbox_type = 'imap' config['imap'] = True if self._folders_received: config['folders'] = self._get_selected_folders() config['idle'] = self._chk_account_push.get_active() elif acctype == IDX_MBOX: mailbox_type = 'mbox' name = self._entry_account_name.get_text() config['folders'] = [] config['path'] = self._chooser_account_file_path.get_filename() elif acctype == IDX_MAILDIR: mailbox_type = 'maildir' name = self._entry_account_name.get_text() config['path'] = self._chooser_account_directory_path.get_filename() config['folders'] = [] if self._folders_received: config['folders'] = self._get_selected_folders() else: # known provider (imap only) mailbox_type = 'imap' name = self._entry_account_user.get_text() config['user'] = self._entry_account_user.get_text() config['password'] = self._entry_account_password.get_text() config['ssl'] = True config['imap'] = True config['folders'] = [] if self._folders_received: config['folders'] = self._get_selected_folders() config['idle'] = (len(config['folders']) < 2) if acctype < len(PROVIDER_CONFIGS): p = PROVIDER_CONFIGS[acctype] name += (' (%s)' % p[0]) config['server'] = p[1] config['port'] = p[2] else: raise Exception('Unknown account type') acc.set_config( mailbox_type=mailbox_type, enabled=acc.enabled, name=name, config=config) 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(_("IMAP (Custom)")) self._cmb_account_type.append_text(_("POP3 (Custom)")) self._cmb_account_type.append_text(_("MBox (Custom)")) self._cmb_account_type.append_text(_("Maildir (Custom)")) config = self._acc.get_config() # select account type if self._acc.mailbox_type == '': # default to Gmail when creating new accounts idx = IDX_GMAIL else: idx = -1 if 'user' in config and 'server' in config and 'port' in config: user = config['user'] server = config['server'] port = config['port'] for i, p in enumerate(PROVIDER_CONFIGS): if (('%s (%s)' % (user, p[0])) == self._acc.name) and \ p[1] == server and p[2] == port: idx = i break if idx < 0: if self._acc.mailbox_type == 'imap': idx = IDX_IMAP elif self._acc.mailbox_type == 'pop3': idx = IDX_POP3 elif self._acc.mailbox_type == 'mbox': idx = IDX_MBOX elif self._acc.mailbox_type == 'maildir': idx = IDX_MAILDIR else: # This is actually error case, but recovering to IMAP idx = IDX_IMAP self._cmb_account_type.set_active(idx) # triggers _on_cmb_account_type_changed() # Don't allow changing the account type if the loaded account has folders. if 'folders' in config: is_type_change_allowed = len(config['folders']) == 0 else: is_type_change_allowed = True self._cmb_account_type.set_sensitive(is_type_change_allowed) 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 elif acctype == IDX_MBOX: ok = len(self._entry_account_name.get_text()) > 0 and \ (self._chooser_account_file_path.get_filename() is not None) elif acctype == IDX_MAILDIR: ok = len(self._entry_account_name.get_text()) > 0 and \ (self._chooser_account_directory_path.get_filename() is not None) 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_ok.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_password_info_icon_released(self, widget, icon_pos, event): Gtk.show_uri_on_window(None, GMAIL_SUPPORT_PAGE, Gdk.CURRENT_TIME) 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() self._entry_account_password.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None) # # 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_user.set_visible(True) self._entry_account_user.set_visible(True) self._label_account_password.set_visible(True) self._entry_account_password.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) self._label_account_file_path.set_visible(False) self._chooser_account_file_path.set_visible(False) self._label_account_directory_path.set_visible(False) self._chooser_account_directory_path.set_visible(False) elif acctype == IDX_IMAP: self._label_account_name.set_visible(True) self._entry_account_name.set_visible(True) self._label_account_user.set_visible(True) self._entry_account_user.set_visible(True) self._label_account_password.set_visible(True) self._entry_account_password.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) self._label_account_file_path.set_visible(False) self._chooser_account_file_path.set_visible(False) self._label_account_directory_path.set_visible(False) self._chooser_account_directory_path.set_visible(False) elif acctype == IDX_MBOX: self._label_account_name.set_visible(True) self._entry_account_name.set_visible(True) self._label_account_user.set_visible(False) self._entry_account_user.set_visible(False) self._label_account_password.set_visible(False) self._entry_account_password.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(False) self._chk_account_push.set_visible(False) self._chk_account_ssl.set_visible(False) self._label_account_file_path.set_visible(True) self._chooser_account_file_path.set_visible(True) self._label_account_directory_path.set_visible(False) self._chooser_account_directory_path.set_visible(False) elif acctype == IDX_MAILDIR: self._label_account_name.set_visible(True) self._entry_account_name.set_visible(True) self._label_account_user.set_visible(False) self._entry_account_user.set_visible(False) self._label_account_password.set_visible(False) self._entry_account_password.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._label_account_file_path.set_visible(False) self._chooser_account_file_path.set_visible(False) self._label_account_directory_path.set_visible(True) self._chooser_account_directory_path.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_user.set_visible(True) self._entry_account_user.set_visible(True) self._label_account_password.set_visible(True) self._entry_account_password.set_visible(True) 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._label_account_file_path.set_visible(False) self._chooser_account_file_path.set_visible(False) self._label_account_directory_path.set_visible(False) self._chooser_account_directory_path.set_visible(False) if acctype == IDX_GMAIL: self._entry_account_password.set_icon_from_icon_name( Gtk.EntryIconPosition.SECONDARY, self._pwd_info_icon) 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-2.2.0/Mailnag/configuration/configwindow.py000066400000000000000000000311001401226772200224070ustar00rootroot00000000000000# Copyright 2011 - 2021 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 shutil import xdg.BaseDirectory as bd from gi.repository import Gtk 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 from Mailnag.common.config import read_cfg, write_cfg from Mailnag.common.accounts import Account, AccountManager from Mailnag.common.plugins import Plugin from Mailnag.configuration.accountdialog import AccountDialog from Mailnag.configuration.plugindialog import PluginDialog class ConfigWindow: def __init__(self, app): 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_info_clicked" : self._on_btn_info_clicked, \ "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, \ }) self._window = builder.get_object("config_window") self._window.set_icon_name("mailnag") self._window.set_application(app) self._cfg = read_cfg() self._daemon_enabled = False self._switch_daemon_enabled = builder.get_object("switch_daemon_enabled") # # accounts page # self._accountman = AccountManager() 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) 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) 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_all() def get_gtk_window(self): return self._window def get_daemon_enabled(self): return self._daemon_enabled 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 = [s for s in [s.strip() for s in enabled_lst] if s != ''] plugins = Plugin.load_plugins(self._cfg) plugins.sort(key = lambda p : p.get_manifest()[0]) for plugin in plugins: name, desc, ver, author = plugin.get_manifest() enabled = True if (plugin.get_modname() in enabled_lst) 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.items(): 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() == Gtk.ResponseType.OK: 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 = os.path.join(autostart_folder, "mailnag.desktop") if os.path.exists(autostart_file): os.remove(autostart_file) def _on_btn_info_clicked(self, widget): aboutdialog = Gtk.AboutDialog() aboutdialog.set_title(_("About %s") % PACKAGE_NAME.title()) aboutdialog.set_version(APP_VERSION) aboutdialog.set_program_name(PACKAGE_NAME.title()) aboutdialog.set_comments(_("An extensible mail notification daemon.")) aboutdialog.set_copyright(_("Copyright (c) 2011 - 2021 Patrick Ulbrich and contributors.")) aboutdialog.set_logo_icon_name("mailnag") aboutdialog.set_website("https://github.com/pulb/mailnag") aboutdialog.set_website_label(_("Homepage")) aboutdialog.set_license_type(Gtk.License.GPL_2_0) aboutdialog.set_authors([ "Patrick Ulbrich (maintainer)", "Andreas Angerer", "Balló György", "Dan Christensen", "Denis Anuschewski", "Edwin Smulders", "Freeroot", "James Powell", "Leighton Earl", "Matthias Mailänder", "Oleg", "Ralf Hersel", "razer", "Taylor Braun-Jones", "Thomas Haider", "Timo Kankare", "Vincent Cheng" ]) aboutdialog.set_translator_credits(_("translator-credits")) aboutdialog.set_artists([ "Reda Lazri" ]) aboutdialog.connect("response", lambda w, r: aboutdialog.destroy()) aboutdialog.set_modal(True) aboutdialog.set_transient_for(self._window) aboutdialog.show() 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() == Gtk.ResponseType.OK: 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 _on_config_window_deleted(self, widget, event): self._save_config() self._daemon_enabled = self._switch_daemon_enabled.get_active() mailnag-2.2.0/Mailnag/configuration/plugindialog.py000066400000000000000000000033121401226772200223740ustar00rootroot00000000000000# Copyright 2013 - 2019 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 self._window = Gtk.Dialog(title = _('Plugin Configuration'), parent = parent, use_header_bar = True, \ buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)) self._window.set_default_response(Gtk.ResponseType.OK) self._window.set_default_size(480, 0) self._box = self._window.get_content_area() self._box.set_border_width(12) self._box.set_spacing(12) def run(self): widget = self._plugin.get_config_ui() if widget != None: self._box.pack_start(widget, True, True, 0) 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-2.2.0/Mailnag/daemon/000077500000000000000000000000001401226772200157415ustar00rootroot00000000000000mailnag-2.2.0/Mailnag/daemon/__init__.py000066400000000000000000000013701401226772200200530ustar00rootroot00000000000000# 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-2.2.0/Mailnag/daemon/conntest.py000066400000000000000000000030441401226772200201510ustar00rootroot00000000000000# Copyright 2014 - 2021 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 from enum import Enum from gi.repository import Gio TEST_HOST = 'www.google.com' class TestModes(Enum): NETWORKMONITOR = 0 PING = 1 class ConnectivityTest: def __init__(self, testmode): self._testmode = testmode self._monitor = None def is_offline(self): if self._testmode == TestModes.NETWORKMONITOR: if self._monitor == None: # The monitor instance is based on NetworkManager if available, # otherwise on the kernels netlink interface. self._monitor = Gio.NetworkMonitor.get_default() if self._monitor.get_connectivity() == Gio.NetworkConnectivity.FULL: return False else: return (not self._monitor.can_reach(Gio.NetworkAddress.new(TEST_HOST, 8080))) else: return (os.system('ping -c1 -W2 %s > /dev/null 2>&1' % TEST_HOST) != 0) mailnag-2.2.0/Mailnag/daemon/dbus.py000066400000000000000000000066311401226772200172560ustar00rootroot00000000000000# Copyright 2013 - 2020 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 import logging from Mailnag.common.dist_cfg import DBUS_BUS_NAME, DBUS_OBJ_PATH from Mailnag.common.exceptions import InvalidOperationException MAX_INT32 = ((0xFFFFFFFF // 2) - 1) # DBUS server that exports Mailnag signals and methods class DBusService(dbus.service.Object): def __init__(self, mailnag_daemon): self._mails = [] self._daemon = mailnag_daemon bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus = dbus.SessionBus()) dbus.service.Object.__init__(self, bus_name, DBUS_OBJ_PATH) @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._daemon.shutdown() except InvalidOperationException: pass @dbus.service.method(dbus_interface = DBUS_BUS_NAME) def CheckForMails(self): try: self._daemon.check_for_mails() except InvalidOperationException: pass @dbus.service.method(dbus_interface = DBUS_BUS_NAME, in_signature = 's') def MarkMailAsRead(self, mail_id): self._mails = [m for m in self._mails if m['id'] != mail_id] try: self._daemon.mark_mail_as_read(mail_id) except InvalidOperationException: pass def signal_mails_added(self, new_mails, all_mails): conv_new_mails = self._convert_mails(new_mails) conv_all_mails = self._convert_mails(all_mails) self._mails = conv_all_mails self.MailsAdded(conv_new_mails, conv_all_mails) def signal_mails_removed(self, remaining_mails): conv_remaining_mails = self._convert_mails(remaining_mails) self._mails = conv_remaining_mails self.MailsRemoved(conv_remaining_mails) def _convert_mails(self, mails): converted_mails = [] for m in mails: d = {} name, addr = m.sender if m.datetime > MAX_INT32: logging.warning('dbusservice: datetime out of range (mailnag dbus api uses int32 timestamps).') datetime = 0 else: datetime = m.datetime d['datetime'] = 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 mailnag-2.2.0/Mailnag/daemon/idlers.py000066400000000000000000000136541401226772200176060ustar00rootroot00000000000000# Copyright 2011 - 2021 Patrick Ulbrich # Copyright 2016, 2018 Timo Kankare # 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.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 self._disposing = False self._disposed = False def start(self): if self._disposed: raise InvalidOperationException("Idler has been disposed") self._thread.start() def dispose(self): if self._thread.is_alive(): # flag a shutdown self._disposing = True # unblock idle event the _idle thread is waiting for self._event.set() self._thread.join() self._disposed = True logging.info('Idler closed') # idle thread def _idle(self): # mailbox may have been opened in mailnagdaemon.py already (immediate check) while (not self._account.is_open()) and (not self._disposing): try: self._account.open() except Exception as ex: logging.error("Failed to open mailbox 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 while not self._disposing: self._needsync = False self._needreset = False try: self._account.notify_next_change(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() except: logging.exception('Caught an exception.') # Reset current connection if the call to notify_next_change() fails # as this is probably connection related (e.g. conn terminated). self._reset_conn() # if the event is set due to idle sync if self._needsync: if self._needreset: self._reset_conn() # NOTE: the event must be cleared after resetting (closing) the connection # so no more callbacks (that may set the event again) can occur. self._event.clear() if self._account.is_open(): try: # Fetch and sync mails from account self._sync_callback(self._account) except: logging.exception('Caught an exception.') # Reset current connection if the call to sync_callback() (mail sync) fails. # An immediate sync has already been performed successfully on startup, # so if an error occurs here, it may be connection related (e.g. conn terminated). self._reset_conn() self._account.cancel_notifications() # idle callback (runs on a further thread) def _idle_callback(self, error): # flag that a mail sync is needed self._needsync = True # flag the need for a connection reset in case of an error (e.g. conn terminated) if error is not None: error_type, error_val = error logging.error("Idle callback for account '%s' returned an error (%s - %s)." % (self._account.name, error_type, str(error_val))) self._needreset = True # trigger waiting _idle thread self._event.set() def _reset_conn(self): # Try to reset the connection to recover from a possible connection error (e.g. after system suspend) logging.info("Resetting connection for account '%s'" % self._account.name) try: self._account.close() except: pass self._reconnect() 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) while (not self._account.is_open()) and (not self._disposing): logging.info("Trying to reconnect Idler thread for account '%s'." % self._account.name) try: self._account.open() 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 def _wait(self, secs): start_time = time.time() while (((time.time() - start_time) < secs) and (not self._disposing)): 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.supports_notifications(): 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-2.2.0/Mailnag/daemon/mailchecker.py000066400000000000000000000074541401226772200205740ustar00rootroot00000000000000# Copyright 2011 - 2020 Patrick Ulbrich # Copyright 2020 Andreas Angerer # 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, dbus_service): self._firstcheck = True # first check after startup self._mailcheck_lock = threading.Lock() self._mailsyncer = MailSyncer(cfg) self._mailbox_seen_flags = bool(cfg.get('core', 'mailbox_seen_flags')) self._memorizer = memorizer self._hookreg = hookreg self._conntest = conntest self._dbus_service = dbus_service self._count_on_last_check = 0 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 = [] seen_mails_by_account = {} 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) elif self._mailbox_seen_flags: # if the mail account supports tagging mails as seen (e.g. IMAP), # mark the mail as seen on the server as well. if mail.account.supports_mark_as_seen(): if not mail.account in seen_mails_by_account: seen_mails_by_account[mail.account] = [] seen_mails_by_account[mail.account].append(mail) else: # mail is fetched the first time unseen_mails.append(mail) new_mails.append(mail) # Flag mails to seen on server for acc, mails in seen_mails_by_account.items(): acc.mark_as_seen(mails) 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: self._dbus_service.signal_mails_added(filtered_new_mails, filtered_unseen_mails) for f in self._hookreg.get_hook_funcs(HookTypes.MAILS_ADDED): try_call( lambda: f(filtered_new_mails, filtered_unseen_mails) ) elif len(filtered_unseen_mails) != self._count_on_last_check: self._dbus_service.signal_mails_removed(filtered_unseen_mails) for f in self._hookreg.get_hook_funcs(HookTypes.MAILS_REMOVED): try_call( lambda: f(filtered_unseen_mails) ) self._count_on_last_check = len(filtered_unseen_mails) return mailnag-2.2.0/Mailnag/daemon/mailnagdaemon.py000066400000000000000000000174731401226772200211230ustar00rootroot00000000000000# Copyright 2016 Timo Kankare # Copyright 2014 - 2021 Patrick Ulbrich # Copyright 2020 Andreas Angerer # # 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.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.daemon.dbus import DBusService 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.NETWORKMONITOR, # Legacy, deprecated 'networkmanager' : TestModes.NETWORKMONITOR, # Legacy, deprecated 'networkmonitor' : TestModes.NETWORKMONITOR, 'ping' : TestModes.PING } class MailnagDaemon(MailnagController): def __init__(self, fatal_error_handler = None, shutdown_request_handler = None): self._fatal_error_handler = fatal_error_handler self._shutdown_request_handler = shutdown_request_handler self._plugins = [] self._poll_thread = None self._poll_thread_stop = threading.Event() self._idlrunner = None self._disposed = False self._cfg = read_cfg() accountman = AccountManager() 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')]) self._memorizer = Memorizer() self._memorizer.load() dbus_service = DBusService(self) self._mailchecker = MailChecker(self._cfg, self._memorizer, self._hookreg, self._conntest, dbus_service) # 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) # Start checking for mails asynchronously. self._start_thread = threading.Thread(target = self._start) self._start_thread.start() def dispose(self): 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.is_open(): try: acc.close() except: pass self._unload_plugins() def is_disposed(self): return self._disposed # Part of MailnagController interface def get_hooks(self): # Note: ensure_not_disposed() cannot be called here # because plugins are calling get_hooks() when being disabled in dispose(). # self._ensure_not_disposed() return self._hookreg # Part of MailnagController interface def shutdown(self): if self._shutdown_request_handler != None: self._shutdown_request_handler() # Part of MailnagController interface def mark_mail_as_read(self, mail_id): # Note: ensure_not_disposed() is not really necessary here # (the memorizer object is available in dispose()), # but better be consistent with other daemon methods. self._ensure_not_disposed() self._memorizer.set_to_seen(mail_id) self._memorizer.save() # Enforces manual mail checks # Part of MailnagController interface def check_for_mails(self): # Don't allow mail checks after object disposal. # F.i. plugins may not be unloaded completely or # connections may have been closed already. self._ensure_not_disposed() non_idle_accounts = self._get_non_idle_accounts(self._accounts) self._mailchecker.check(non_idle_accounts) def _ensure_not_disposed(self): 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 = self._get_idle_accounts(self._accounts) non_idle_accounts = self._get_non_idle_accounts(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(): while True: self._poll_thread_stop.wait(timeout = 60.0 * poll_interval) if self._poll_thread_stop.is_set(): break try: 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): self._mailchecker.check([account]) 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 _get_idle_accounts(self, accounts): return [acc for acc in self._accounts if acc.supports_notifications()] def _get_non_idle_accounts(self, accounts): return [acc for acc in self._accounts if not acc.supports_notifications()] def _load_plugins(self, cfg): enabled_lst = cfg.get('core', 'enabled_plugins').split(',') enabled_lst = [s for s in [s.strip() for s in enabled_lst] if s != ''] self._plugins = Plugin.load_plugins(cfg, self, 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-2.2.0/Mailnag/daemon/mails.py000066400000000000000000000213401401226772200174200ustar00rootroot00000000000000# Copyright 2011 - 2021 Patrick Ulbrich # Copyright 2020 Andreas Angerer # Copyright 2016, 2018 Timo Kankare # Copyright 2011 Leighton Earl # Copyright 2011 Ralf Hersel # Copyright 2019 razer # # 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, make_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, flags): self.datetime = datetime self.subject = subject self.sender = sender self.account = account self.id = id self.flags = flags # # 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: # open mailbox for this account try: if not acc.is_open(): acc.open() except Exception as ex: logging.error("Failed to open mailbox for account '%s' (%s)." % (acc.name, ex)) continue try: for folder, msg, flags in acc.list_messages(): 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, flags)) mail_ids[id] = None except Exception as ex: # Catch exceptions here, so remaining accounts will still be checked # if a specific account has issues. # # Re-throw the exception for accounts that support notifications (i.e. imap IDLE), # so the calling idler thread can handle the error and reset the connection if needed (see idlers.py). # NOTE: Idler threads always check single accounts (i.e. len(self._accounts) == 1), # so there are no remaining accounts to be checked for now. if acc.supports_notifications(): raise else: logging.error("An error occured while processing mails of account '%s' (%s)." % (acc.name, ex)) finally: # leave account with notifications open, so that it can # send notifications about new mails if not acc.supports_notifications(): # disconnect from Email-Server acc.close() # sort mails if sort: mail_list.sort(key = lambda m: m.datetime, reverse = True) return mail_list def _get_header(self, msg_dict): # Get sender sender = ('', '') try: content = self._get_header_field(msg_dict, 'From') # get the two parts of the sender addr = email.utils.parseaddr(content) if len(addr) != 2: logging.warning('Malformed sender field in message.') else: sender_real = self._convert(addr[0]) sender_addr = self._convert(addr[1]) sender = (sender_real, sender_addr) except: pass # Get subject try: content = self._get_header_field(msg_dict, 'Subject') subject = self._convert(content) except: subject = _('No subject') # Get date try: content = self._get_header_field(msg_dict, 'Date') # make a 10-tupel (UTC) parsed_date = email.utils.parsedate_tz(content) # convert 10-tupel to seconds incl. timezone shift datetime = email.utils.mktime_tz(parsed_date) except: logging.warning('Email date set to zero.') datetime = 0 # Get message id 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 # return utf-8 decoded string from multi-part/multi-charset header text def _convert(self, text): decoded = decode_header(text) if not decoded[0][1] or 'unknown' in decoded[0][1]: decoded = [(decoded[0][0], 'latin-1')] return str(make_header(decoded)) 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.get_id() + folder + 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.get_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.keys(): if acc_id in tmp: del_ids = [] for mail_id in self._mails_by_account[acc_id].keys(): 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(key = lambda m: m.datetime, reverse = True) return self._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 list(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 list(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-2.2.0/Mailnag/plugins/000077500000000000000000000000001401226772200161575ustar00rootroot00000000000000mailnag-2.2.0/Mailnag/plugins/libnotifyplugin.py000066400000000000000000000304201401226772200217460ustar00rootroot00000000000000# Copyright 2013 - 2020 Patrick Ulbrich # Copyright 2020 Dan Christensen # Copyright 2020 Denis Anuschewski # # 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 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 = self._is_gnome_environment(('XDG_CURRENT_DESKTOP', 'GDMSESSION')) 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 def mails_removed_hook(remaining_mails): self._notify_async([], remaining_mails) self._mails_removed_hook = mails_removed_hook controller = self.get_mailnag_controller() hooks = controller.get_hooks() 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): 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 # 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."), "2.1", "Patrick Ulbrich ") 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_SINGLE: self._notify_single(new_mails, all_mails) else: if len(all_mails) == 0: if '0' in self._notifications: # The user may have closed the notification: try_close(self._notifications['0']) del self._notifications['0'] elif len(new_mails) > 0: 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) 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, new_mails, all_mails): # Remove notifications for messages not in all_mails: for k, n in list(self._notifications.items()): if hasattr(n, 'mail') and not (n.mail in all_mails): # The user may have closed the notification: try_close(n) del self._notifications[k] # In single notification mode new mails are # added to the *bottom* of the notification list. new_mails.sort(key = lambda m: m.datetime, reverse = False) for mail in new_mails: n = self._get_notification(self._get_sender(mail), mail.subject, "mail-unread") # Remember the associated message, so we know when to remove the notification: n.mail = mail 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.values(): try_close(n) 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 _is_gnome_environment(self, env_vars): for var in env_vars: if 'gnome' in os.environ.get(var, '').lower().split(':'): return True return False 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] + '...' # If the user has closed the notification, an exception is raised. def try_close(notification): try: notification.close() except: pass mailnag-2.2.0/Mailnag/plugins/soundplugin.py000066400000000000000000000055661401226772200211140ustar00rootroot00000000000000# Copyright 2013 - 2020 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."), "2.0", "Patrick Ulbrich ") 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-2.2.0/Mailnag/plugins/spamfilterplugin.py000066400000000000000000000073621401226772200221260ustar00rootroot00000000000000# Copyright 2013 - 2020 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."), "2.0", "Patrick Ulbrich ") 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-2.2.0/Mailnag/plugins/userscriptplugin.py000066400000000000000000000070741401226772200221630ustar00rootroot00000000000000# Copyright 2013 - 2020 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."), "2.0", "Patrick Ulbrich ") 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, False, False, 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-2.2.0/NEWS000066400000000000000000000170501401226772200136300ustar00rootroot00000000000000Version 2.2 (2021-02-14): ========================= * Massive robustness improvements. Especially long running IMAP IDLE connections do survive peer resets and system hibernation now * New default connectivity test based on glib NetworkMonitor (likely to improve VPN issues as well) * Configuration UI hints for Gmail * Updated translations NOTES FOR PACKAGERS: The networkmanager dependency may be removed since Mailnag now uses the NetworkMonitor API (of glib), which in turn optionally depends on networkmanager. Version 2.1 (2020-12-30): ========================= * Mark-as-read also marks e-mails read on the server if supported (thanks Andreas Angerer!) * libnotify plugin: close associated notification when the e-mail has been read (single-notification-mode only, thanks Dan Christensen!) * libnofify plugin: correctly identify desktop environment (thanks Denis Anuschewski!) * Misc other fixes and improvements * Updated translations Version 2.0 (2020-03-28): =========================== * Ported to Python3 * Migrated from Gnome keyring to libsecret * Moved functionality of dbus plugin to mailnag core * Added support for CRAM-MD5 auth * Added STARTTLS support for pop3 accounts * Cosmetic UI fixes * Misc other fixes and improvements IMPORTANT NOTES FOR PACKAGERS: Please note that mailnag now requires python >= 3.5 and that the GNOME-Keyring dependency has been replaced by libsecret. Version 1.3 (2019-03-24): =========================== * UI modernization including headerbars * Ported to GtkApplication * Support for mbox and maildir backends (thanks tikank!) * Massive refactoring (thanks tikank!) * Support for UTF-7 folder decoding (thanks cheshire-mouse!) * Improved folder parsing in config UI * Misc other fixes and improvements * Updated translations Version 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-2.2.0/README.md000066400000000000000000000111751401226772200144120ustar00rootroot00000000000000![Screenshot](https://raw.githubusercontent.com/pulb/mailnag-design/master/Flyer/Mailnag_flyer2.png) ## 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://hosted.weblate.org/projects/mailnag/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 available in Debian stable and 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 official repos. Please run `pacman -Syu mailnag` (as root) to install the package. ### openSUSE Mailnag is available in openSUSE Tumbleweed. Run `sudo zypper install mailnag` in a terminal to install it. ### Generic Tarballs Distribution independent tarball releases are available [here](https://github.com/pulb/mailnag/releases). Just run `./setup.py install` (as root) to install Mailnag, though make sure the requirements stated below are met. ###### Requirements * python (>= 3.5) * 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 * gir1.2-secret-1 (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)) * KDE ([Plasma 5 applet by driglu4it](https://store.kde.org/p/1420222/)) * Cinnamon ([Applet by hyOzd](https://bitbucket.org/hyOzd/mailnagapplet)) * Elementary Pantheon ([MessagingMenu plugin](https://github.com/pulb/mailnag-messagingmenu-plugin)) * XFCE ([MessagingMenu plugin](https://github.com/pulb/mailnag-messagingmenu-plugin)) 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. ### Troubleshooting __Gmail doesn't work__ If Mailnag is unable to connect to your Gmail account, please try the following solutions: * Install the [GOA plugin](https://github.com/pulb/mailnag-goa-plugin) to connect via GNOME online accounts * Have a look at the [FAQ](https://github.com/pulb/mailnag/wiki/FAQ) * Try to apply [this](https://github.com/pulb/mailnag/issues/190) workaround __Other issues__ If Mailnag doesn't work properly for you, either examine the system log for errors (`journalctl -b _COMM=mailnag`) or run `mailnag` in a terminal and observe the output. mailnag-2.2.0/data/000077500000000000000000000000001401226772200140375ustar00rootroot00000000000000mailnag-2.2.0/data/account_widget.ui000066400000000000000000000302071401226772200173770ustar00rootroot00000000000000 True False 6 6 6 True True True number 1 5 True True True 1 4 True True True False emblem-important You may need to create an application-specific password for Gmail. Click this icon for more information. 1 3 True True True 1 2 True True True 1 1 Enable Push-IMAP True True False 0 True 0 8 2 True False 1 0 Enable SSL encryption True True False 0 True 0 9 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 0 -1 True False Folders (optional) 0 10 2 True False File path: 0 0 6 True False False False True False 1 6 True False Directory: 0 0 7 True False select-folder False False True False 1 7 mailnag-2.2.0/data/config_window.ui000066400000000000000000000321151401226772200172340ustar00rootroot00000000000000 False center 0 320 True False 0 True True True Enable/disable Mailnag daemon 1 True False 32 32 stack1 True True True popovermenu True False open-menu-symbolic end 1 True False slide-left-right 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 page0 Accounts 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 page1 Plugins 1 False 6 menubutton1 True False vertical Donate True True True none https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8F5FNJ3U4N7AW False True 2 True True True Info True False True 3 submenu0 1 mailnag-2.2.0/data/icons/000077500000000000000000000000001401226772200151525ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/000077500000000000000000000000001401226772200166115ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/128x128/000077500000000000000000000000001401226772200175465ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/128x128/apps/000077500000000000000000000000001401226772200205115ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/128x128/apps/mailnag.png000066400000000000000000000220651401226772200226340ustar00rootroot00000000000000PNG  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-2.2.0/data/icons/hicolor/16x16/000077500000000000000000000000001401226772200173765ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/16x16/apps/000077500000000000000000000000001401226772200203415ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/16x16/apps/mailnag.png000066400000000000000000000011331401226772200224550ustar00rootroot00000000000000PNG  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-2.2.0/data/icons/hicolor/22x22/000077500000000000000000000000001401226772200173705ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/22x22/apps/000077500000000000000000000000001401226772200203335ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/22x22/apps/mailnag.png000066400000000000000000000017201401226772200224510ustar00rootroot00000000000000PNG  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-2.2.0/data/icons/hicolor/48x48/000077500000000000000000000000001401226772200174105ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/48x48/apps/000077500000000000000000000000001401226772200203535ustar00rootroot00000000000000mailnag-2.2.0/data/icons/hicolor/48x48/apps/mailnag.png000066400000000000000000000054071401226772200224770ustar00rootroot00000000000000PNG  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-2.2.0/data/mailnag-config.1000066400000000000000000000011201401226772200167660ustar00rootroot00000000000000.TH MAILNAG-CONFIG "1" "Mar 2020" "Mailnag 2.0.0" .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-2.2.0/data/mailnag-config.desktop000066400000000000000000000006051401226772200203060ustar00rootroot00000000000000 [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-2.2.0/data/mailnag.1000066400000000000000000000014041401226772200155300ustar00rootroot00000000000000.TH MAILNAG "1" "Mar 2020" "Mailnag 2.0.0" .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-2.2.0/data/mailnag.appdata.xml000066400000000000000000000034721401226772200176100ustar00rootroot00000000000000 mailnag.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 Patrick Ulbrich https://github.com/pulb/mailnag/issues https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8F5FNJ3U4N7AW https://github.com/pulb/mailnag/wiki https://translations.launchpad.net/mailnag
mailnag-2.2.0/data/mailnag.desktop000066400000000000000000000005171401226772200170450ustar00rootroot00000000000000 [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 Categories=Network;Email; X-GNOME-Autostart-enabled=true X-GNOME-UsesNotifications=true mailnag-2.2.0/data/mailnag.ogg000066400000000000000000000356361401226772200161620ustar00rootroot00000000000000OggSaSvorbisOggSa5=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-2.2.0/gen_locales000077500000000000000000000010201401226772200153200ustar00rootroot00000000000000#/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-2.2.0/gen_po_template000077500000000000000000000010731401226772200162170ustar00rootroot00000000000000#!/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-2.2.0/mailnag000077500000000000000000000074431401226772200144740ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2011 - 2020 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 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, init_logging, 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 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 sigterm_handler(mainloop): if mainloop != None: mainloop.quit() def main(): mainloop = GLib.MainLoop() daemon = None set_procname(PROGNAME) 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(enable_stdout = (not args.quiet), \ enable_syslog = True, log_level = LOG_LEVEL) 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() # Initialize mailnag daemon and start checking threads daemon = MailnagDaemon( fatal_error_hdlr, shutdown_request_hdlr) # 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-2.2.0/mailnag-config000077500000000000000000000047731401226772200157420ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2011 - 2019 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 import subprocess import logging from gi.repository import Gtk from Mailnag.common.utils import fix_cwd, init_logging fix_cwd() from Mailnag.common.i18n import _ from Mailnag.common.utils import set_procname, shutdown_existing_instance, get_data_file, get_data_paths from Mailnag.common.dist_cfg import BIN_DIR from Mailnag.configuration.configwindow import ConfigWindow LOG_LEVEL = logging.DEBUG class App(Gtk.Application): def __init__(self): Gtk.Application.__init__(self, application_id = 'com.github.pulp.Mailnag') self.win = None def do_startup(self): Gtk.Application.do_startup(self) # 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")) def do_activate(self): Gtk.Application.do_activate(self) if not self.win: self.win = ConfigWindow(self) self.win.get_gtk_window().present() def do_shutdown(self): Gtk.Application.do_shutdown(self) if self.win.get_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: # shutdown running Mailnag daemon shutdown_existing_instance(wait_for_completion = False) def main(): set_procname("mailnag-config") init_logging(enable_stdout = True, enable_syslog = False, log_level = LOG_LEVEL) app = App() app.run(None) if __name__ == "__main__": main() mailnag-2.2.0/po/000077500000000000000000000000001401226772200135445ustar00rootroot00000000000000mailnag-2.2.0/po/bg.po000066400000000000000000000206041401226772200144760ustar00rootroot00000000000000# Bulgarian translation for mailnag # Copyright (c) 2017 Rosetta Contributors and Canonical Ltd 2017 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \n" "Language-Team: Bulgarian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Потребителски скрипт" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "При пристигане на имейл стартира скрипт дефиниран от потребителя" #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "акаунт" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "изпращач" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "тема" #: Mailnag/plugins/userscriptplugin.py:81 #, 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" "Mailnag ще предава общия брой имейли на този скрипт,\n" "последвано от %s поредици." #: Mailnag/plugins/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Максимален брой видими имейли." #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Спам филтър" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Филтър за нежелана поща." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Брой на нови писма" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Кратка извадка от новите писма" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Подробна извадка от новите писма" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Едно известие за имейл" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Режим на известяване:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} нови писма" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "от {0} и други." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "от {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Ново писмо" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "( и {0} други)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Отбележи като прочетено" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Звукови известия" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Изпълнява звук при пристигане на имейл." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "GNOME онлайн акаунти" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Интеграция с GNOME онлайн акаунти." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Без тема" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Настройки на приставките" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Включено" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Име" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " spacy01 https://launchpad.net/~spacy00001" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Изтриване на акаунта:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Пощенски акаунт" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "незадължително" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Връзката се разпадна." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Включи Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Включи SSL кодиране" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Име на акаунт:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Вид акаунт:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Потребител:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Парола:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Сървър:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Порт:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Папки (не задължително)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Добави акаунт" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Премахни акаунт" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Редактирай акаунт" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Акаунти" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Редактирай плъгин" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Плъгини" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/ca.po000066400000000000000000000177631401226772200145050ustar00rootroot00000000000000# Catalan translation for mailnag # Copyright (c) 2020 Rosetta Contributors and Canonical Ltd 2020 # This file is distributed under the same license as the mailnag package. # FIRST AUTHOR , 2020. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2020-04-05 12:44+0000\n" "Last-Translator: Marc Riera Irigoyen \n" "Language-Team: Catalan \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Script de l'usuari" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Executa un script definit per l'usuari quan arriba correu nou." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "compte" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "remitent" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "assumpte" #: Mailnag/plugins/userscriptplugin.py:81 #, 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 "" "S'executarà l'script següent quan arribin correus nous.\n" "El Mailnag passa el nombre total de correus nous a aquest script,\n" "seguit de seqüències %s." #: Mailnag/plugins/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "Menú de missatges" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "Mostra els correus nous a l'indicador del Menú de missatges." #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Nombre màxim de correus visibles:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Filtre de correu brossa" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Exclou correus no desitjats." #: Mailnag/plugins/spamfilterplugin.py:87 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "El Mailnag ignorarà els correus que continguin com a mínim \n" "una de les paraules següents a l'assumpte o el remitent." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "Notificacions del LibNotify" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Mostra una finestra emergent quan arriben correus nous." #: Mailnag/plugins/libnotifyplugin.py:116 msgid "Count of new mails" msgstr "Recompte de correus nous" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Resum breu dels correus nous" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Resum detallat dels correus nous" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Una notificació per correu nou" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Mode de notificació:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} correus nous" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "de {0} i altres." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "de {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Correu nou" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(i {0} més)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Marca com a llegit" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Notificacions sonores" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Reprodueix un so quan arriben correus nous." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Comptes en línia del GNOME" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integració amb els comptes en línia del GNOME." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Sense assumpte" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Configuració del connector" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Habilitat" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nom" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "Quant al %s" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "Un dimoni de notificacions de correu extensible." #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "Lloc web" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Marc Riera Irigoyen https://launchpad.net/~marcriera" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Suprimeix aquest compte:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Compte de correu" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "IMAP (personalitzat)" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "POP3 (personalitzat)" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "MBox (personalitzat)" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "Maildir (personalitzat)" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "No s'ha pogut connectar." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Habilita el Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Habilita el xifratge SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nom del compte:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Tipus de compte:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Usuari:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Contrasenya:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Servidor:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Port:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Carpetes (opcional)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "Camí del fitxer:" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "Directori:" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "Habilita/inhabilita el dimoni del Mailnag" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Afegeix un compte" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Suprimeix el compte" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Edita el compte" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Comptes" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Edita el connector" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Connectors" #: data/config_window.ui.h:8 msgid "Donate" msgstr "Feu una donació" #: data/config_window.ui.h:9 msgid "Info" msgstr "Informació" mailnag-2.2.0/po/cs.po000066400000000000000000000173331401226772200145200ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" "Language: cs\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Uživatelský skript" #: Mailnag/plugins/userscriptplugin.py:61 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:80 msgid "account" msgstr "účet" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "odesílatel" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "předmět" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maximální počet viditelných e-mailů:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Spam filtr" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtruje nevyžádané e-maily." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Počet nových e-mailů" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Krátký přehled nových e-mailů" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Detailní přehled nových e-mailů" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Jedna notifikace pro nový e-mail" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Notifikační mód:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} nových e-mailů" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "z {0} a další." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "z {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Nový mail" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(a {0} dalších)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Označit jako přečtený" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Zvukové notifikace" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Přehraje zvuk při novém e-mailu." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "GNOME Online účty" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integrace GNOME Online účtů." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Žádný předmět" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Konfigurace pluginu" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Povoleno" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Název" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Jirka Dutka https://launchpad.net/~jirka-x\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Radek Otáhal https://launchpad.net/~radek-otahal" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Odstranit tento účet:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Poštovní účet" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "volitelně" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Spojení selhalo." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Povolit Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Povolit SSL encryption" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Název účtu:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Typ účtu:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Uživatel:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Heslo:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Server:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Port:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Složky (volitelně)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Přidat účet" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Odebrat účet" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Upravit účet" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Účty" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Upravit plugin" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Pluginy" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/de.po000066400000000000000000000201571401226772200145010ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2020-10-24 19:26+0000\n" "Last-Translator: J. Lavoie \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.3.2-dev\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Benutzerskript" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Führt ein benutzerdefiniertes Skript bei E-Mail-Ankunft aus." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "Konto" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "Absender" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "Betreff" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "MessagingMenu" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "Zeigt neue E-Mails im MessagingMenu-Indikator an." #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maximale Anzahl sichtbarer neuer E-Mails:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Spamfilter" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtert unerwünschte E-Mails heraus." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Anzahl neuer E-Mails" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Kurze Zusammenfassung neuer E-Mails" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Detaillierte Zusammenfassung neuer E-Mails" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Eine Benachrichtigung pro neuer E-Mail" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Benachrichtigungsmodus:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} neue E-Mails" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "von {0} und anderen." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "von {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Neue Nachricht" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(und {0} weitere)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Als gelesen markieren" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Klangbenachrichtigungen" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Spielt einen Klang ab, wenn neue E-Mails eintreffen." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "GNOME Online Accounts" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integration von GNOME Online Accounts." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Kein Betreff" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Plugin-Konfiguration" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Aktiviert" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Name" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "Über %s" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "Ein erweiterbarer Benachrichtigungs-Dämon." #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "Copyright (c) 2011 - 2020 Patrick Ulbrich und Mitwirkende." #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "Startseite" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Tobias Bannert https://launchpad.net/~toba" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Dieses Konto löschen:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "E-Mail-Konto" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "optional" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "IMAP (Benutzerdefiniert)" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "POP3 (Benutzerdefiniert)" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "MBox (Benutzerdefiniert)" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "Maildir (Benutzerdefiniert)" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Verbindung fehlgeschlagen." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Push-IMAP aktivieren" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "SSL-Verschlüsselung aktivieren" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Kontoname:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Kontotyp:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Benutzer:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Passwort:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Server:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Port:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Ordner (optional)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "Dateipfad:" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "Verzeichnis:" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "Mailnag-Dämon ein-/ausschalten" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Konto hinzufügen" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Konto löschen" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Konto bearbeiten" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Konten" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Plugin bearbeiten" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Plugins" #: data/config_window.ui.h:8 msgid "Donate" msgstr "Spenden" #: data/config_window.ui.h:9 msgid "Info" msgstr "Infos" mailnag-2.2.0/po/es.po000066400000000000000000000173401401226772200145200ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2020-10-24 19:26+0000\n" "Last-Translator: J. Lavoie \n" "Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.3.2-dev\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Secuencia de órdenes de usuario" #: Mailnag/plugins/userscriptplugin.py:61 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:80 msgid "account" msgstr "cuenta" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "remitente" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "asunto" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Cantidad máxima de mensajes visibles:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Filtro de correo no deseado" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtra correos no deseados." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Contador de mensajes nuevos" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Una notificación por mensaje nuevo" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Modo de notificación:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} mensajes nuevos" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Mensaje nuevo" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(y {0} más)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Marcar como leído" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Notificaciones con sonido" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Reproduce un sonido al llegar un mensaje nuevo." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Cuentas en línea de GNOME" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integración con las cuentas en línea de GNOME." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Sin asunto" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Configuración de complementos" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Activado" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nombre" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Adolfo Jayme https://launchpad.net/~fitojb\n" " Marti Bosch https://launchpad.net/~martini1\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Wolter Hellmund https://launchpad.net/~wolterh\n" " dagavi https://launchpad.net/~dagavi\n" " javiggvv https://launchpad.net/~javigrisalena-q" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Eliminar esta cuenta:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Cuenta de correo" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "" #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Activar Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Activar cifrado SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nombre de la cuenta:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Tipo de cuenta:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Usuario:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Contraseña:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Servidor:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Puerto:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Añadir una cuenta" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Eliminar la cuenta" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Editar la cuenta" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Cuentas" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Editar el complemento" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Complementos" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/fr.po000066400000000000000000000212201401226772200145100ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2020-10-15 17:26+0000\n" "Last-Translator: J. Lavoie \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 4.3-dev\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Script utilisateur" #: Mailnag/plugins/userscriptplugin.py:61 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:80 msgid "account" msgstr "compte" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "expéditeur" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "objet" #: Mailnag/plugins/userscriptplugin.py:81 #, 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 "" "Le script suivant sera exécuté à l’arrivée de nouveaux courriels.\n" "Mailnag transmet le nombre total de nouveaux courriels à ce script,\n" "suivi des séquences %s." #: Mailnag/plugins/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "MenuMessagerie" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "Affiche les nouveaux courriels dans l'indicateur MenuMessagerie." #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Nombre maximum de courriels visibles :" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Filtre anti-pourriel" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtre les courriels non désirés." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Nombre de nouveaux courriels" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Résumé court des nouveaux courriels" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Résumé détaillé des nouveaux courriels" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Une notification par nouveau courriel" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Mode de notification :" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} nouveaux courriels" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "de {0} et d’autres." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "de {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Nouveau courriel" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(et {0} de plus)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Marquer comme lu" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Notifications sonores" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Joue un son à l’arrivée d’un nouveau courriel." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "GNOME Online Accounts" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Intégration des comptes en ligne GNOME." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Sans objet" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Configuration du greffon" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Activé" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nom" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "À propos de %s" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "Un démon de notification de courriel extensible." #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "Copyright © 2011 – 2020 Patrick Ulbrich et les contributeurs." #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "Page d’accueil" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Alexandre Singh https://launchpad.net/~darthwound\n" " Charles Monzat https://launchpad.net/~superboa\n" " Jean-Marc https://launchpad.net/~m-balthazar\n" " LEROY Jean-Christophe https://launchpad.net/~celtic2-deactivatedaccount\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Philippe Poumaroux https://launchpad.net/~poum-g\n" " Zeppelinlg https://launchpad.net/~zeppelinlg\n" " poulp https://launchpad.net/~mathieu-nerv\n" " tamplan https://launchpad.net/~tamplan" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Supprimer ce compte :" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Compte de messagerie" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "facultatif" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "IMAP (personnalisé)" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "POP3 (personnalisé)" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "MBox (personnalisé)" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "Maildir (personnalisé)" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Échec de connexion." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Activer le Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Activer le chiffrement SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nom du compte :" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Type de compte :" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Utilisateur :" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Mot de passe :" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Serveur :" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Port :" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Dossiers (optionnel)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "Chemin de fichier :" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "Répertoire :" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "Activer/désactiver le démon Mailnag" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Ajouter un compte" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Supprimer le compte" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Modifier le compte" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Comptes" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Modifier le greffon" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Greffons" #: data/config_window.ui.h:8 msgid "Donate" msgstr "Faire un don" #: data/config_window.ui.h:9 msgid "Info" msgstr "Infos" mailnag-2.2.0/po/gl.po000066400000000000000000000165631401226772200145210ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Script do usuario" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Executa un script do usuario ao recibir algún correo." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "remitente" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "asunto" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Número máximo de mensaxes visibles:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Filtro de spam" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtra os correos non desexados." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Total de novos correos" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Unha notificación por cada novo correo" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Modo Notificación:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} novos correos" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Correo novo" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(e {0} máis)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Marcar como lido" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Notificacións con son" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Reproduce un son ao recibir un novo correo-e" #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Sen asunto" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Configuración do complemento" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Activado" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nome" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Manuel Xosé Lemos https://launchpad.net/~mxlemos\n" " Marcos Lans https://launchpad.net/~markooss\n" " Patrick Ulbrich https://launchpad.net/~pulb" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Eliminar esta conta:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Conta de correo-e" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "" #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Activar Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Activar cifrado SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nome da conta:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Tipo de conta:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Usuario:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Contrasinal:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Servidor:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Porto:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Engadir conta" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Retirar conta" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Editar conta" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Contas" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Editar complemento" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Complementos" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/hr.po000066400000000000000000000205051401226772200145170ustar00rootroot00000000000000# Croatian 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. # nenad smart , 2017. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2021-01-18 00:35+0000\n" "Last-Translator: Milo Ivir \n" "Language-Team: Croatian \n" "Language: hr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 4.5-dev\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Korisnička skripta" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Pokreće korisnički definiranu skriptu po dolasku e-poruka." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "račun" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "pošiljatelj" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "predmet" #: Mailnag/plugins/userscriptplugin.py:81 #, 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 "" "Sljedeća skripta izvršit će se kad god stignu nove e-poruke.\n" "Mailnag prosljeđuje ukupan broj novih e-poruka ovoj skripti,\n" "nakon čega slijede %s sekvence." #: Mailnag/plugins/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "Izbornik poruka" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "Prikazuje nove e-poruke u indikatoru izbornika poruka." #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maksimalan broj vidljivih e-poruka:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "filter za spam poruke" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtrira neželjene e-poruke." #: Mailnag/plugins/spamfilterplugin.py:87 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag će zanemariti e-poruke koje sadrže barem jednu od\n" "sljedećih riječi u predmetu ili pošiljatelju." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "LibNotify oznake" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Prikazuje skočni prozor po primanju novih e-poruka." #: Mailnag/plugins/libnotifyplugin.py:116 msgid "Count of new mails" msgstr "Broj novih e-poruka" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Kratak pregled novih e-poruka" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Detaljan pregled novih e-poruka" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Jedna obavijest za svaku novu e-poruku" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Modus obavještavanja:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "Broj novih e-poruka: {0}" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "od {0} i drugih." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "od {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Nova e-poruka" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(i još {0})" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Označi kao pročitano" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Obavještavanje zvukom" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Svira zvuk po primanju nove e-poruke." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Internetski GNOME računi" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integracija internetskih GNOME računa." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Bez predmeta" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "podešavanje priključka" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Omogućeno" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Ime" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "Informacije o %s" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "Proširiv demon za obavještavanje o e-porukama." #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "Autorska prava 2011. – 2020. Patrick Ulbrich i doprinositelji." #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "Početna web-stranica" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Ivo Majić https://launchpad.net/~ivo.majic\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " gogo https://launchpad.net/~trebelnik-stefina\n" " nenad smart https://launchpad.net/~genijalno" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Izbriši ovaj račun:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Račun e-pošte" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opcionalno" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "IMAP (prilagođeno)" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "POP3 (prilagođeno)" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "MBox (prilagođeno)" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "Maildir (prilagođeno)" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Neuspjelo povezivanje." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "omoguči Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "omoguči SSL enkripciju" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Naziv korisničkog računa:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Vrsta računa:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Korisnik:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Lozinka:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Poslužitelj:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Ulaz:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Mape(neobavezno)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "Staza datoteke:" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "Direktorij:" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "Aktiviraj/deaktiviraj Mailnag demon" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Dodaj račun" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Ukloni račun" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Uredi račun" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Korisnički računi" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "uredi priključke" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Priključci" #: data/config_window.ui.h:8 msgid "Donate" msgstr "Doniraj" #: data/config_window.ui.h:9 msgid "Info" msgstr "Informacije" mailnag-2.2.0/po/id.po000066400000000000000000000165631401226772200145130ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" "Language: id\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Skrip Pengguna" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Jalankan skrip tentuan pengguna saat surat datang." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "pengirim" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "subyek" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Jumlah maksimum surat terlihat:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Penyaring Spam" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Saring surat yang tidak diinginkan." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Jumlah surat baru" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Ringkasan singkat surel baru" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Ringkasan detail surel baru" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Satu notifikasi per surat baru" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Mode notifikasi:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} surat baru" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "dari {0} dan lainnya." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "dari {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Surat baru" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(dan {0} lainnya)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Tandai sudah dibaca" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Notifikasi Suara" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Putar suara ketika surat baru datang." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Akun Daring GNOME" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integrasi Akun Daring GNOME." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Tidak ada subyek" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Konfigurasi Plugin" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Aktifkan" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nama" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " zmni https://launchpad.net/~zmni-deactivatedaccount" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Hapus akun ini:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Akun Surat" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opsional" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Koneksi gagal." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Aktifkan Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Aktifkan Enkripsi SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nama akun:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Jenis akun:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Pengguna:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Sandi:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Server:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Port:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Folder (opsional)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Tambah Akun" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Buang Akun" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Sunting Akun" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Akun" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Sunting Plugin" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Plugin" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/it.po000066400000000000000000000201671401226772200145260ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2020-10-15 17:26+0000\n" "Last-Translator: J. Lavoie \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.3-dev\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\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:60 msgid "User Script" msgstr "Script utente" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Esegue uno script definito dall'utente all'avvio." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "account" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "Mittente" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "Oggetto" #: Mailnag/plugins/userscriptplugin.py:81 #, 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 "" "Il seguente script verrà eseguito ogni volta che nuove e-mail arrivano.\n" "Mailnag passa il conteggio totale dei nuovi messaggi a questo script,\n" "seguito da %s sequenze." #: Mailnag/plugins/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "MessagingMenu" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "Mostra i nuovi messaggi nell'indicatore MessagingMenu." #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Massimo numero di e-mail visibili:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Filtro spam" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtra le e-mail indesiderate." #: Mailnag/plugins/spamfilterplugin.py:87 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag ignorerà le e-mail contenenti almeno una delle\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 nuove e-mail." #: Mailnag/plugins/libnotifyplugin.py:116 msgid "Count of new mails" msgstr "Numero di nuove e-mail" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Breve riepilogo delle nuove e-mail" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Riepilogo dettagliato delle nuove e-mail" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Una notifica ogni nuova e-mail" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Modalità di notifica:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} nuove e-mail" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "da {0} e altri." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "da {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Nuova e-mail" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(e altri {0})" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Contrassegna come \\\"già letto\\\"" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Notifiche audio" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Riproduci un suono all'arrivo di nuove e-mail." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "GNOME Online Accounts" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integrazione con GNOME Online Accounts." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Nessun oggetto" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Configurazione plugin" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Attivo" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nome" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "Informazioni su %s" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "Un demone di notifica di e-mail estensibile." #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "Copyright © 2011 – 2020 Patrick Ulbrich e i collaboratori." #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "Pagina principale" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Fabrizio Papa https://launchpad.net/~fabri-pope\n" " Mattia M. https://launchpad.net/~hal8600\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Pedro Beja https://launchpad.net/~althaser" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Eliminare questo account:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Account di posta" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opzionale" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "IMAP (personalizzato)" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "POP3 (personalizzato)" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "MBox (personalizzato)" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "Maildir (personalizzato)" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Connessione fallita." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Attiva il protocollo Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Attiva il protocollo crittografico SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nome dell'account:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Tipo di account:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Nome utente:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Password:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Server:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Porta:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Cartelle (opzionale)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "Percorso del file:" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "Directory:" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "Abilita / disabilita il demone Mailnag" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Aggiungi account" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Rimuovi account" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Modifica account" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Account" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Modifica plugin" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Plugin" #: data/config_window.ui.h:8 msgid "Donate" msgstr "Dona" #: data/config_window.ui.h:9 msgid "Info" msgstr "Informazioni" mailnag-2.2.0/po/mailnag.pot000066400000000000000000000145741401226772200157130ustar00rootroot00000000000000# 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: 2021-02-01 17:31+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:60 msgid "User Script" msgstr "" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "" #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "" #: Mailnag/plugins/spamfilterplugin.py:87 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:113 msgid "LibNotify Notifications" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:114 msgid "Shows a popup when new mails arrive." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:129 msgid "Count of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:130 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:131 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:132 msgid "One notification per new mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:140 msgid "Notification mode:" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:233 #: Mailnag/plugins/libnotifyplugin.py:269 #: Mailnag/plugins/libnotifyplugin.py:306 #, python-brace-format msgid "{0} new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:235 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:237 #: Mailnag/plugins/libnotifyplugin.py:240 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:239 #: Mailnag/plugins/libnotifyplugin.py:271 #: Mailnag/plugins/libnotifyplugin.py:308 msgid "New mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:264 #: Mailnag/plugins/libnotifyplugin.py:266 #, python-brace-format msgid "(and {0} more)" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:295 msgid "Mark as read" msgstr "" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "" #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/daemon/mails.py:136 msgid "No subject" msgstr "" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:118 msgid "Enabled" msgstr "" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:124 msgid "Name" msgstr "" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2021 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:298 msgid "translator-credits" msgstr "" #: Mailnag/configuration/configwindow.py:338 msgid "Delete this account:" msgstr "" #: Mailnag/configuration/accountdialog.py:72 msgid "Mail Account" msgstr "" #: Mailnag/configuration/accountdialog.py:114 msgid "optional" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:248 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:249 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:250 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:356 msgid "Connection failed." msgstr "" #: data/account_widget.ui.h:1 msgid "" "You may need to create an application-specific password for Gmail.\n" "Click this icon for more information." msgstr "" #: data/account_widget.ui.h:3 msgid "Enable Push-IMAP" msgstr "" #: data/account_widget.ui.h:4 msgid "Enable SSL encryption" msgstr "" #: data/account_widget.ui.h:5 msgid "Accountname:" msgstr "" #: data/account_widget.ui.h:6 msgid "Account type:" msgstr "" #: data/account_widget.ui.h:7 msgid "User:" msgstr "" #: data/account_widget.ui.h:8 msgid "Password:" msgstr "" #: data/account_widget.ui.h:9 msgid "Server:" msgstr "" #: data/account_widget.ui.h:10 msgid "Port:" msgstr "" #: data/account_widget.ui.h:11 msgid "Folders (optional)" msgstr "" #: data/account_widget.ui.h:12 msgid "File path:" msgstr "" #: data/account_widget.ui.h:13 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/pl.po000066400000000000000000000171431401226772200145250ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Skrypt użytkownika" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Uruchom skrypt użytkownika gdy nadejdzie nowa poczta." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "konto" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "nadawca" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "temat" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maksymalna ilość widocznych wiadomości:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Filtr spamowy" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtruje niechciane wiadomości." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Liczba nowych wiadomości" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Krótkie podsumowanie nowych wiadomości e-mail" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Szczegółowe podsumowanie nowych wiadomości e-mail" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Jedno powiadomienie dla jednej wiadomości" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Tryb powiadamiania:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} nowych wiadomości" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "od {0} i innych." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "od {0}" #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Nowa poczta" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(i {0} więcej)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Oznacz jako przeczytana" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Dżwięk powiadomienia" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Odtwarzaj dźwięk kiedy przychodzi nowa poczta." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Konto GNOME Online" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integracja z kontem GNOME Online" #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Brak tematu" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Konfiguracja wtyczki" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Włączone" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nazwa" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Maciej https://launchpad.net/~pan-efem\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Piotr Filipek https://launchpad.net/~piotrek290\n" " RapierTG https://launchpad.net/~rapier\n" " Szymon Nieznański https://launchpad.net/~isamu715\n" " vbert https://launchpad.net/~wsobczak" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Usuń to konto:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Konto pocztowe" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opcjonalny" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Połączenie nie powiodło się." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Włącz Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Włącz szyfrowanie SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nazwa konta:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Typ konta:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Użytkownik:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Hasło:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Serwer:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Port:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Foldery (opcjonalne)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Dodaj konto" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Usuń konto" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Zmodyfikuj konto" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Konta" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Zmodyfikuj wtyczkę" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Wtyczki" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/pt.po000066400000000000000000000173461401226772200145420ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Script de Utilizador" #: Mailnag/plugins/userscriptplugin.py:61 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:80 msgid "account" msgstr "conta" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "remetente" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "assunto" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Número máximo de mails visíveis:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Filtro Spam" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtra mails indesejados." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Quantidade de mails novos" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Resumo curto de mails novos" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Resumo detalhado de mails novos" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Uma notificação por novo mail" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Modo de notificação:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} novos mails" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "de {0} e outros." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "de {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Novo mail" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(e mais {0})" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Marcar como lido" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Notificações de Som" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Toca um som quando chegam novos mails." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Contas Online no GNOME" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integração de Contas Online no GNOME." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Sem assunto" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Configuração de Plugin" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Ativado" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nome" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Lidinei https://launchpad.net/~lidinei-gmail\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Pedro Beja https://launchpad.net/~althaser\n" " Rafael Neri https://launchpad.net/~rafepel" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Apagar esta conta:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Conta de mail" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "A ligação falhou." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Ativar Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Ativar encriptação SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nome da Conta:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Tipo de conta:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Utilizador:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Password:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Servidor:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Porta:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Pastas (opcional)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Adicionar Conta" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Remover Conta" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Editar Conta" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Contas" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Editar Plugin" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Plugins" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/pt_BR.po000066400000000000000000000174731401226772200151260ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-12-25 19:25+0000\n" "Last-Translator: Relaxeaza \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Script de Usuário" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Executa um script definido pelo usuário na chegada de mensagens." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "conta" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "remetente" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "assunto" #: Mailnag/plugins/userscriptplugin.py:81 #, 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 será executado sempre que cheguarem novas mensagens.\n" "Mailnag passa o total novas mensagens para o script,\n" "seguido por sequências de %s." #: Mailnag/plugins/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "MessagingMenu" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "Monstrar novas mensagens no indicador MessagingMenu." #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Número máximo de mensagens visíveis:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Filtro de Spam" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtra mensagens indesejadas." #: Mailnag/plugins/spamfilterplugin.py:87 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag ignorará mensagens 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 novas mensagens." #: Mailnag/plugins/libnotifyplugin.py:116 msgid "Count of new mails" msgstr "Quantidade de mensagens novas" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Resumo curto de mensagens novas" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Resumo detalhado de mensagens novas" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Uma notificação por mensagem nova" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Modo de notificação:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} novas mensagens" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "de {0} e outros." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "de {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Novo mail" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(e mais {0})" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Marcar como lido" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Notificações de Som" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Toca um som quando novas mensagens chegarem." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Contas Online no GNOME" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integração de Contas Online no GNOME." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Sem assunto" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Configuração de Plugin" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Ativado" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Nome" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Pedro Beja https://launchpad.net/~althaser\n" " Rafael Neri https://launchpad.net/~rafepel\n" " Relaxeaza https://launchpad.net/~relaxeaza" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Apagar esta conta:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Conta de mail" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "opcional" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "A ligação falhou." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Habilitar Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Habilitar criptografia SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Nome da conta:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Tipo de conta:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Usuário:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Senha:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Servidor:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Porta:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Pastas (opcional)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Adicionar Conta" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Remover Conta" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Editar Conta" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Contas" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Editar Plugin" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Plugins" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/ru.po000066400000000000000000000212631401226772200145360ustar00rootroot00000000000000# Mailnag russian tranlation # This file is distributed under the same license as the mailnag package. # Oleg , 2016. # msgid "" msgstr "" "Project-Id-Version: mailnag\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" "Language: RU\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Пользовательский скрипт" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Запустить пользовательский скрипт при получении почты" #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "учетная запись" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "отправитель" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "тема" #: Mailnag/plugins/userscriptplugin.py:81 #, 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" "Скрипту передаётся количество сообщений,\n" "за которым следуют: %s" #: Mailnag/plugins/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Максимальное количество отображаемых писем:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Спам фильтр" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Фильтрует нежелательные сообщения." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Количество новых писем" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Краткие сведения о письмах" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Подробные сведения о письмах" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Одно уведомление на письмо" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Тип уведомлений:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "сообщений: {0}" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "от {0} и других." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "от {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Новое сообщение" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(и еще {0})" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Отметить как прочитанное" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Звуковые уведомления" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Проигрывает мелодию при появлении нового сообщения." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Без темы" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Настройка плагина" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Включено" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Имя" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Dmitry Shachnev https://launchpad.net/~mitya57\n" " Hromin https://launchpad.net/~hromin\n" " Oleg https://launchpad.net/~cheshire-mouse\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Vyacheslav Sharmanov https://launchpad.net/~vsharmanov\n" " u-t https://launchpad.net/~fenoform" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Удалить учетную запись:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Учетная запись" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "необязательно" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Ошибка соединения." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Включить Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Включить SSL шифрование" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Имя учетной записи:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Тип учетной записи:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Пользователь:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Пароль:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Сервер:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Порт:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Папки (необзязательно)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Добавить учетную запись" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Удалить учетную запись" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Редактировать учетную запись" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Учетные записи" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Редактировать плагин" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Плагины" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/sr.po000066400000000000000000000204571401226772200145400ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2021-01-07 11:29+0000\n" "Last-Translator: Burek \n" "Language-Team: Serbian \n" "Language: sr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 4.4.1-dev\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Корисничка скрипта" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Покреће скрипту коју је одредио корисник када пристигне пошта." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "налог" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "пошиљалац" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "тема" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Највећи број видљивих поштанских порука:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Пропусник спама" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Издваја нежељену пошту." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Број нових поштанских порука" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Кратак сажетак нове поште" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Опширан сажетак нове поште" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Једно обавештење по новој пошти" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Режим обавештавања:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "нових поштанских порука: {0}" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "шаље {0} и други." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "шаље {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Нова пошта" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(и још {0})" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Означи као прочитано" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Звучно обавештавање" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Пушта звук када стигне нова пошта." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Гномови налози на мрежи" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Обједињавање Гномових налога на мрежи." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Без теме" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Подешавање прикључка" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Укључено" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Назив" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "преводилац/иоци-заслуге" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Обриши ове налоге:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Налог поште" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "изборно" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Веза није успела." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Укључи Гурни-ИМАП" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Укључи ССЛ шифровање" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Назив налога:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Врста налога:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Корисник:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Лозинка:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Сервер:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Прикључник:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Фасцикле (изборно)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Додај налог" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Уклони налог" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Уреди налог" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Налози" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Уреди прикључак" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Прикључци" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/sv.po000066400000000000000000000175001401226772200145370ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2020-12-24 02:29+0000\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.4.1-dev\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Användarskript" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Kör ett användardefinerat skript vid inkommande meddelanden." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "konto" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "avsändare" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "ämne" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Maximalt antal visade meddelanden:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Skräpfilter" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Filtrera bort oönskade mail." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Antal nya meddelanden" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Kort summering av nya meddelanden" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Detaljerad summering av nya meddelanden" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "En avisering per nytt meddelande" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Aviseringsläge" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} nya meddelanden" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "från {0} och andra." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "från {0}." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Nytt meddelande" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(och {0} till)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Markera som läst" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Ljudaviseringar" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Spelar upp ett ljud när nya meddelanden anländer." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "GNOME Nätkonton" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "Integrering med GNOME Nätkonton." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Ämne saknas" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Inställning av insticksprogram" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Aktiverad" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Namn" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "Om %s" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "Hemsida" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Anders Jonsson https://launchpad.net/~anders-jonsson\n" " kristian https://launchpad.net/~kristianm24" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Ta bort detta konto:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "E-postkonto" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "valfritt" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "IMAP (Anpassad)" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "POP3 (Anpassad)" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "MBox (Anpassad)" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "Maildir (Anpassad)" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Anslutning misslyckades." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Aktivera Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Aktivera SSL-kryptering" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Kontonamn:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Kontotyp:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Användare:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Lösenord:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Server:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Port:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Mappar (valfritt)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "Katalog:" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Lägg till Konto" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Ta bort konto" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Redigera konto" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Konton" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Redigera instick" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Tillägg" #: data/config_window.ui.h:8 msgid "Donate" msgstr "Donera" #: data/config_window.ui.h:9 msgid "Info" msgstr "Info" mailnag-2.2.0/po/tr.po000066400000000000000000000201571401226772200145360ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2020-10-10 22:26+0000\n" "Last-Translator: Oğuz Ersen \n" "Language-Team: Turkish \n" "Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.3-dev\n" "X-Launchpad-Export-Date: 2020-06-11 14:44+0000\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Kullanıcı Betiği" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "" "E-posta geldiğinde kullanıcı tarafından tanımlanan bir betik çalıştırır." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "hesap" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "gönderen" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "konu" #: Mailnag/plugins/userscriptplugin.py:81 #, 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 "" "Yeni e-posta geldiğinde aşağıdaki betik çalıştırılacaktır.\n" "Mailnag, yeni e-postaların toplam sayısını, ve ardından\n" "%s dizisini bu betiğe iletir." #: Mailnag/plugins/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "MessagingMenu" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "Yeni e-postaları MessagingMenu göstergesinde gösterir." #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Görüntülenecek en fazla e-posta sayısı:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Spam Filtresi" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "İstenmeyen postaları filtreler." #: Mailnag/plugins/spamfilterplugin.py:87 msgid "" "Mailnag will ignore mails containing at least one of \n" "the following words in subject or sender." msgstr "" "Mailnag, konu veya gönderen kısmında aşağıdaki sözcüklerden\n" "en az birini içeren e-postaları dikkate almayacaktır." #: Mailnag/plugins/libnotifyplugin.py:100 msgid "LibNotify Notifications" msgstr "LibNotify Bildirimleri" #: Mailnag/plugins/libnotifyplugin.py:101 msgid "Shows a popup when new mails arrive." msgstr "Yeni e-posta geldiğinde bir açılır pencere gösterir." #: Mailnag/plugins/libnotifyplugin.py:116 msgid "Count of new mails" msgstr "Yeni e-posta sayısı" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "Yeni e-postaların kısa özeti" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "Yeni e-postaların ayrıntılı özeti" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Her yeni e-posta için bir bildirim" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Bildirim modu:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} yeni e-posta" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "{0} ve diğerlerinden." #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "{0}'den." #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Yeni e-posta" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(ve {0} tane daha)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Okundu olarak işaretle" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Sesli Bildirimler" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Yeni e-posta geldiğinde bir ses çalar." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "GNOME Çevrim İçi Hesaplar" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "GNOME Çevrim İçi Hesaplar Bütünleşmesi." #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Konu yok" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Eklenti Yapılandırması" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Etkin" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Adı" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "%s hakkında" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "Genişletilebilir bir e-posta bildirim arka plan programı." #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "Telif hakkı (c) 2011 - 2020 Patrick Ulbrich ve katkıda bulunanlar." #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "Ana sayfa" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Aydın Yakar https://launchpad.net/~yakar\n" "Oğuz Ersen " #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Bu hesabı sil:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "E-posta Hesabı" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "isteğe bağlı" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "IMAP (Özel)" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "POP3 (Özel)" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "MBox (Özel)" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "Maildir (Özel)" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Bağlantı başarısız oldu." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Push-IMAP Etkinleştir" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "SSL şifrelemeyi etkinleştir" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Hesap adı:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Hesap türü:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Kullanıcı:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Parola:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Sunucu:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Bağlantı noktası:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "Klasörler (isteğe bağlı)" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "Dosya yolu:" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "Dizin:" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "Mailnag arka plan programını etkinleştir/devre dışı bırak" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Hesap Ekle" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Hesabı Sil" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Hesabı Düzenle" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Hesaplar" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Eklentiyi Düzenle" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Eklentiler" #: data/config_window.ui.h:8 msgid "Donate" msgstr "Bağış Yap" #: data/config_window.ui.h:9 msgid "Info" msgstr "Bilgi" mailnag-2.2.0/po/uk.po000066400000000000000000000204011401226772200145200ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "Користувацький скріпт" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "Виконувати скріпт користувача при отриманні листа." #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "обліківка" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "відправник" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "тема" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "Максимальна к-сть листів у списку:" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "Фільтр спаму" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "Відфільтровує небажані листи." #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "Кількість нових листів" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "Про кожен лист окремо" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "Метод сповіщення:" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} листів" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "Новий лист" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "(і ще {0})" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "Позначити як прочитане" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "Звукові сповіщення" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "Супроводжує нові листи звуковим сповіщенням." #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "Мережеві облікові записи у GNOME" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "Без теми" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Налаштування додатка" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "Задіяно" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "Назва" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Andrii Prokopenko https://launchpad.net/~anprok\n" " Mykola Tkach https://launchpad.net/~stuartlittle1970\n" " Oleg «Eleidan» Kulyk https://launchpad.net/~helh-saintman\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " Rax https://launchpad.net/~r-a-x" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "Видалити обліковий запис" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "Обліковий запис пошти" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "(необов’язково)" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "Не вдалося встановити з'єднання." #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "Задіяти Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "Увімкнути шифрування SSL" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "Назва облікового запису:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "Тип облікового запису:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "Користувач:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "Пароль:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "Сервер:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "Порт" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "Додати обліковий запис" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "Видалити обліковий запис" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "Редагувати обліковий запис" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "Облікові записи" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "Налаштувати додаток" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "Додатки" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/zh_CN.po000066400000000000000000000160731401226772200151140ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "用户脚本" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "运行在邮件到达时的用户自定义脚本。" #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "寄件人" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "主题" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "可显示邮件的最大数量" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "垃圾邮件过滤器" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "过滤掉不想要的邮件。" #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "新邮件数量" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "每封新邮件通知一次" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "通知模式" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "( 还有 {0} 条)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "标记为已读" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "声音通知" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "当新邮件到达时播放声音。" #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "无主题" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "已启用" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "名称" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " Hu Meng https://launchpad.net/~humeng\n" " Patrick Ulbrich https://launchpad.net/~pulb\n" " 朱涛 https://launchpad.net/~bill-zt" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "删除该帐户:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "邮件账户" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "可选" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "" #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "启用 Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "启用 SSL 加密" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "账户名称:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "账户类型:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "用户名:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "密码:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "服务器:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "端口:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "帐户" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/po/zh_TW.po000066400000000000000000000162151401226772200151440ustar00rootroot00000000000000# 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: 2020-03-28 10:59+0100\n" "PO-Revision-Date: 2019-03-16 14:48+0000\n" "Last-Translator: Launchpad Translations Administrators \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: 2020-06-11 14:44+0000\n" "X-Generator: Launchpad (build b190cebbf563f89e480a8b57f641753c8196bda0)\n" #: Mailnag/plugins/userscriptplugin.py:60 msgid "User Script" msgstr "用戶腳本" #: Mailnag/plugins/userscriptplugin.py:61 msgid "Runs an user defined script on mail arrival." msgstr "運行在郵件到達時的用戶自定義腳本。" #: Mailnag/plugins/userscriptplugin.py:80 msgid "account" msgstr "" #: Mailnag/plugins/userscriptplugin.py:80 msgid "sender" msgstr "寄件人" #: Mailnag/plugins/userscriptplugin.py:80 msgid "subject" msgstr "主題" #: Mailnag/plugins/userscriptplugin.py:81 #, 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/messagingmenuplugin.py:99 msgid "MessagingMenu" msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:100 msgid "Shows new mails in the MessagingMenu indicator." msgstr "" #: Mailnag/plugins/messagingmenuplugin.py:118 msgid "Maximum number of visible mails:" msgstr "可顯示郵件的最大數量" #: Mailnag/plugins/spamfilterplugin.py:67 msgid "Spam Filter" msgstr "垃圾郵件過濾器" #: Mailnag/plugins/spamfilterplugin.py:68 msgid "Filters out unwanted mails." msgstr "過濾掉不想要的郵件。" #: Mailnag/plugins/spamfilterplugin.py:87 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:116 msgid "Count of new mails" msgstr "新郵件數量" #: Mailnag/plugins/libnotifyplugin.py:117 msgid "Short summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:118 msgid "Detailed summary of new mails" msgstr "" #: Mailnag/plugins/libnotifyplugin.py:119 msgid "One notification per new mail" msgstr "每封新郵件通知一次" #: Mailnag/plugins/libnotifyplugin.py:127 msgid "Notification mode:" msgstr "通知模式" #: Mailnag/plugins/libnotifyplugin.py:214 #: Mailnag/plugins/libnotifyplugin.py:250 #: Mailnag/plugins/libnotifyplugin.py:278 #, python-brace-format msgid "{0} new mails" msgstr "{0} 封新郵件" #: Mailnag/plugins/libnotifyplugin.py:216 #, python-brace-format msgid "from {0} and others." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:218 #: Mailnag/plugins/libnotifyplugin.py:221 #, python-brace-format msgid "from {0}." msgstr "" #: Mailnag/plugins/libnotifyplugin.py:220 #: Mailnag/plugins/libnotifyplugin.py:252 #: Mailnag/plugins/libnotifyplugin.py:280 msgid "New mail" msgstr "新郵件" #: Mailnag/plugins/libnotifyplugin.py:245 #: Mailnag/plugins/libnotifyplugin.py:247 #, python-brace-format msgid "(and {0} more)" msgstr "( 還有 {0} 條)" #: Mailnag/plugins/libnotifyplugin.py:267 msgid "Mark as read" msgstr "標記爲已讀" #: Mailnag/plugins/soundplugin.py:63 msgid "Sound Notifications" msgstr "聲音通知" #: Mailnag/plugins/soundplugin.py:64 msgid "Plays a sound when new mails arrive." msgstr "當新郵件到達時播放聲音。" #: Mailnag/plugins/goaplugin.py:91 msgid "GNOME Online Accounts" msgstr "" #: Mailnag/plugins/goaplugin.py:92 msgid "GNOME Online Accounts Integration." msgstr "" #: Mailnag/daemon/mails.py:121 msgid "No subject" msgstr "無主題" #: Mailnag/configuration/plugindialog.py:30 msgid "Plugin Configuration" msgstr "Plugin 配置" #: Mailnag/configuration/configwindow.py:82 #: Mailnag/configuration/configwindow.py:102 #: Mailnag/configuration/accountdialog.py:115 msgid "Enabled" msgstr "已啓用" #: Mailnag/configuration/configwindow.py:88 #: Mailnag/configuration/configwindow.py:108 #: Mailnag/configuration/accountdialog.py:121 msgid "Name" msgstr "名稱" #: Mailnag/configuration/configwindow.py:270 #, python-format msgid "About %s" msgstr "" #: Mailnag/configuration/configwindow.py:273 msgid "An extensible mail notification daemon." msgstr "" #: Mailnag/configuration/configwindow.py:274 msgid "Copyright (c) 2011 - 2020 Patrick Ulbrich and contributors." msgstr "" #: Mailnag/configuration/configwindow.py:277 msgid "Homepage" msgstr "" #: Mailnag/configuration/configwindow.py:295 msgid "translator-credits" msgstr "" "Launchpad Contributions:\n" " elleryq https://launchpad.net/~elleryq" #: Mailnag/configuration/configwindow.py:335 msgid "Delete this account:" msgstr "刪除該帳戶:" #: Mailnag/configuration/accountdialog.py:70 msgid "Mail Account" msgstr "郵件賬戶" #: Mailnag/configuration/accountdialog.py:111 msgid "optional" msgstr "可選" #: Mailnag/configuration/accountdialog.py:244 msgid "IMAP (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:245 msgid "POP3 (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:246 msgid "MBox (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:247 msgid "Maildir (Custom)" msgstr "" #: Mailnag/configuration/accountdialog.py:353 msgid "Connection failed." msgstr "" #: data/account_widget.ui.h:1 msgid "Enable Push-IMAP" msgstr "啓用 Push-IMAP" #: data/account_widget.ui.h:2 msgid "Enable SSL encryption" msgstr "啓用 SSL 加密" #: data/account_widget.ui.h:3 msgid "Accountname:" msgstr "賬戶名稱:" #: data/account_widget.ui.h:4 msgid "Account type:" msgstr "賬戶類型:" #: data/account_widget.ui.h:5 msgid "User:" msgstr "用戶名:" #: data/account_widget.ui.h:6 msgid "Password:" msgstr "密碼:" #: data/account_widget.ui.h:7 msgid "Server:" msgstr "伺服器:" #: data/account_widget.ui.h:8 msgid "Port:" msgstr "通訊埠:" #: data/account_widget.ui.h:9 msgid "Folders (optional)" msgstr "" #: data/account_widget.ui.h:10 msgid "File path:" msgstr "" #: data/account_widget.ui.h:11 msgid "Directory:" msgstr "" #: data/config_window.ui.h:1 msgid "Enable/disable Mailnag daemon" msgstr "" #: data/config_window.ui.h:2 msgid "Add Account" msgstr "增加帳戶" #: data/config_window.ui.h:3 msgid "Remove Account" msgstr "刪除帳戶" #: data/config_window.ui.h:4 msgid "Edit Account" msgstr "編輯帳戶" #: data/config_window.ui.h:5 msgid "Accounts" msgstr "帳戶" #: data/config_window.ui.h:6 msgid "Edit Plugin" msgstr "編輯 Plugin" #: data/config_window.ui.h:7 msgid "Plugins" msgstr "" #: data/config_window.ui.h:8 msgid "Donate" msgstr "" #: data/config_window.ui.h:9 msgid "Info" msgstr "" mailnag-2.2.0/setup.py000077500000000000000000000104701401226772200146450ustar00rootroot00000000000000#!/usr/bin/env python3 # 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 as 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', 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.backends', 'Mailnag.plugins'], scripts=['mailnag', 'mailnag-config'], data_files=[('share/mailnag', glob.glob('data/*.ui')), ('share/mailnag', ['data/mailnag.ogg']), ('share/mailnag', ['data/mailnag.png']), ('share/metainfo', ['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} ) mailnag-2.2.0/tests/000077500000000000000000000000001401226772200142705ustar00rootroot00000000000000mailnag-2.2.0/tests/test_account.py000066400000000000000000000076471401226772200173530ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # test_account.py # # Copyright 2016, 2018 Timo Kankare # # 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. # """Test cases for Account.""" import pytest from Mailnag.common.accounts import Account def test_account_get_id_should_be_unique(): accounts = [ Account(name='a', mailbox_type='imap', enabled=True, user='x', server='xx'), Account(name='b', mailbox_type='pop3', enabled=True, user='y', server='yy'), Account(name='c', mailbox_type='mbox', enabled=True), Account(name='d', mailbox_type='maildir', enabled=True), ] ids = set(acc.get_id() for acc in accounts) assert len(ids) == len(accounts) def test_account_get_id_should_be_consistent(): account = Account(name='a', mailbox_type='imap', enabled=True, user='x', server='xx') expected_id = account.get_id() for i in range(20): assert account.get_id() == expected_id def test_account_should_keep_configuration(): account = Account(enabled=True, name='my name', user='who', password='secret', oauth2string='who knows', server='example.org', port='1234', ssl=True, imap=True, idle=True, folders=['a', 'b'], mailbox_type='mybox') config = account.get_config() expected_config = { 'enabled': True, 'name': 'my name', 'user': 'who', 'password': 'secret', 'oauth2string': 'who knows', 'server': 'example.org', 'port': '1234', 'ssl': True, 'imap': True, 'idle': True, 'folders': ['a', 'b'], 'mailbox_type': 'mybox', } assert expected_config == config def test_account_should_store_configuration(): new_config = { 'user': 'who', 'password': 'secret', 'oauth2string': 'who knows', 'server': 'example.org', 'port': '1234', 'ssl': True, 'imap': True, 'idle': True, 'folders': ['a', 'b'], } account = Account() account.set_config(mailbox_type='mybox', name='my name', enabled=True, config=new_config) config = account.get_config() expected_config = { 'enabled': True, 'name': 'my name', 'user': 'who', 'password': 'secret', 'oauth2string': 'who knows', 'server': 'example.org', 'port': '1234', 'ssl': True, 'imap': True, 'idle': True, 'folders': ['a', 'b'], 'mailbox_type': 'mybox', } assert expected_config == config def test_account_config_should_always_contain_certain_values(): account = Account() config = account.get_config() assert 'enabled' in config assert 'name' in config assert 'mailbox_type' in config def test_type_should_be_empty_by_default(): account = Account() config = account.get_config() assert account.mailbox_type == '' assert config['mailbox_type'] == '' def test_account_should_configurable_with_any_parameters(): account = Account(weird='odd', odd='weird') config = account.get_config() assert config['weird'] == 'odd' assert config['odd'] == 'weird' @pytest.mark.parametrize("config,should_support", [ ({'mailbox_type': 'imap', 'idle': True}, True), ({'mailbox_type': 'imap', 'idle': False}, False), ({'mailbox_type': 'pop3'}, False), ({'mailbox_type': 'mbox'}, False), ({'mailbox_type': 'maildir'}, False), ]) def test_account_supports_notifications(config, should_support): account = Account(**config) if should_support: assert account.supports_notifications() else: assert not account.supports_notifications() mailnag-2.2.0/tests/test_accountmanager.py000066400000000000000000000173221401226772200206750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # test_accountmanager.py # # Copyright 2016, 2018 Timo Kankare # # 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. # """Test cases for account manager.""" from configparser import RawConfigParser from io import StringIO import pytest from Mailnag.backends import get_mailbox_parameter_specs from Mailnag.common.accounts import AccountManager from Mailnag.common.credentialstore import CredentialStore class FakeCredentialStore(CredentialStore): """Helper class to be used in tests.""" def __init__(self): self.secrets = {} def get(self, key): return self.secrets[key] def set(self, key, secret): self.secrets[key] = secret def remove(self, key): if key in self.secrets: del self.secrets[key] sample_config_file = """ [account1] enabled = 1 name = IMAP mailbox config user = you password = drowssap server = imap.example.org port = ssl = 1 imap = 1 idle = 1 folder = [] [account2] enabled = 1 name = POP3 mailbox config user = me password = poppoppop server = pop.example.org port = ssl = 1 imap = 0 idle = 0 folder = [] [account3] enabled = 1 name = Empty account config for testing default values [account4] enabled = 1 name = Imap config with empty folder option folder = [account5] enabled = 1 name = Imap config with old style folder option folder = folderA, folderB, folderC [account6] enabled = 1 name = Imap config with json folder option folder = ["folderA", "folderB", "folderC"] """ @pytest.fixture def config(): cp = RawConfigParser() cp.readfp(StringIO(sample_config_file), filename='sample_config_file') return cp def test_imap_config_options(config): am = AccountManager() option_spec = get_mailbox_parameter_specs('imap') options = am._get_cfg_options(config, 'account1', option_spec) expected_options = { 'user': 'you', 'password': 'drowssap', 'server': 'imap.example.org', 'port': '', 'ssl': True, 'imap': True, 'idle': True, 'folders': [], } assert expected_options == options def test_imap_config_defaults(config): am = AccountManager() option_spec = get_mailbox_parameter_specs('imap') options = am._get_cfg_options(config, 'account3', option_spec) expected_options = { 'user': '', 'password': '', 'server': '', 'port': '', 'ssl': True, 'imap': True, 'idle': True, 'folders': [], } assert expected_options == options def test_imap_empty_folder_option(config): am = AccountManager() option_spec = get_mailbox_parameter_specs('imap') options = am._get_cfg_options(config, 'account4', option_spec) assert options['folders'] == [] def test_imap_old_folder_option(config): am = AccountManager() option_spec = get_mailbox_parameter_specs('imap') options = am._get_cfg_options(config, 'account5', option_spec) assert options['folders'] == ['folderA', 'folderB', 'folderC'] def test_imap_new_folder_option(config): am = AccountManager() option_spec = get_mailbox_parameter_specs('imap') options = am._get_cfg_options(config, 'account6', option_spec) assert options['folders'] == ['folderA', 'folderB', 'folderC'] def test_pop3_config_options(config): am = AccountManager() option_spec = get_mailbox_parameter_specs('pop3') options = am._get_cfg_options(config, 'account2', option_spec) expected_options = { 'user': 'me', 'password': 'poppoppop', 'server': 'pop.example.org', 'port': '', 'ssl': True, 'imap': False, 'idle': False, } assert expected_options == options def test_pop3_config_defaults(config): am = AccountManager() option_spec = get_mailbox_parameter_specs('pop3') options = am._get_cfg_options(config, 'account3', option_spec) expected_options = { 'user': '', 'password': '', 'server': '', 'port': '', 'ssl': True, 'imap': False, 'idle': False, } assert expected_options == options def test_imap_config_values_should_be_stored(): am = AccountManager() option_spec = get_mailbox_parameter_specs('imap') options = { 'user': 'you', 'password': '', 'server': 'imap.example.org', 'port': '', 'ssl': True, 'imap': True, 'idle': True, 'folders': ['a', 'b'], } config = RawConfigParser() config.add_section('account1') am._set_cfg_options(config, 'account1', options, option_spec) expected_config_items = [ ('user', 'you'), ('password', ''), ('server', 'imap.example.org'), ('port', ''), ('ssl', '1'), ('imap', '1'), ('idle', '1'), ('folder', '["a", "b"]'), ] assert set(expected_config_items) == set(config.items('account1')) # Load from config def get_account(accounts, name): """Finds and returns account which has given name.""" return next(account for account in accounts if account.name == name) def test_load_from_config(config): am = AccountManager() am.load_from_cfg(config, enabled_only=False) accounts = am.to_list() assert len(accounts) == 6 imap_account = get_account(am.to_list(), 'IMAP mailbox config') pop3_account = get_account(am.to_list(), 'POP3 mailbox config') assert imap_account.get_config()['password'] == 'drowssap' assert pop3_account.get_config()['password'] == 'poppoppop' def test_load_from_config_with_credential_store(config): cs = FakeCredentialStore() cs.set('Mailnag password for imap://you@imap.example.org', 'verry seecret') cs.set('Mailnag password for pop://me@pop.example.org', 'seecret too') am = AccountManager(cs) am.load_from_cfg(config, enabled_only=False) accounts = am.to_list() assert len(accounts) == 6 imap_account = get_account(am.to_list(), 'IMAP mailbox config') pop3_account = get_account(am.to_list(), 'POP3 mailbox config') assert imap_account.get_config()['password'] == 'verry seecret' assert pop3_account.get_config()['password'] == 'seecret too' # Save to config def test_save_zero_accounts_to_config(config): am = AccountManager() am.save_to_cfg(config) assert len(config.sections()) == 0 def test_save_all_accounts_to_config(config): am = AccountManager() am.load_from_cfg(config, enabled_only=False) am.save_to_cfg(config) assert len(config.sections()) == 6 def test_save_zero_accounts_to_config_with_credential_store(config): cs = FakeCredentialStore() am = AccountManager(cs) am.save_to_cfg(config) assert len(config.sections()) == 0 assert cs.secrets == {} def test_save_all_accounts_to_config_with_credential_store(config): cs = FakeCredentialStore() cs.set('Mailnag password for imap://you@imap.example.org', 'verry seecret') cs.set('Mailnag password for pop://me@pop.example.org', 'seecret too') am = AccountManager(cs) am.load_from_cfg(config, enabled_only=False) am.save_to_cfg(config) assert len(config.sections()) == 6 assert cs.secrets == { 'Mailnag password for imap://@': '', 'Mailnag password for imap://you@imap.example.org': 'verry seecret', 'Mailnag password for pop://me@pop.example.org': 'seecret too' } def test_save_removed_accounts_to_config_with_credential_store(config): cs = FakeCredentialStore() cs.set('Mailnag password for imap://you@imap.example.org', 'verry seecret') cs.set('Mailnag password for pop://me@pop.example.org', 'seecret too') am = AccountManager(cs) am.load_from_cfg(config, enabled_only=False) am.clear() am.save_to_cfg(config) assert len(config.sections()) == 0 assert cs.secrets == {} mailnag-2.2.0/tests/test_backend_local.py000066400000000000000000000176471401226772200204610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # test_backend_local.py # # Copyright 2016 Timo Kankare # # 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. # """Tests for local backends.""" import mailbox import pytest from Mailnag.backends import create_backend @pytest.fixture def sample_path(tmpdir): """Temporary path for mailbox used in tests.""" return str(tmpdir.join('sample')) class TestMBox: """Tests for mbox backend.""" @pytest.fixture(autouse=True) def sample_mbox(self, sample_path): """Temporary mbox instance for tests.""" return mailbox.mbox(sample_path, create=True) def test_create_mbox_backend(self): be = create_backend('mbox') assert be is not None def test_initially_mailbox_should_be_closed(self): be = create_backend('mbox') assert not be.is_open() def test_mailbox_should_be_open_when_opened(self, sample_path): be = create_backend('mbox', path=sample_path) be.open() assert be.is_open() def test_closed_mailbox_should_be_closed(self, sample_path): be = create_backend('mbox', path=sample_path) be.open() be.close() assert not be.is_open() def test_lists_no_messages_from_empty_mailbox(self, sample_path): be = create_backend('mbox', name='sample', path=sample_path) be.open() try: msgs = list(be.list_messages()) assert len(msgs) == 0 finally: be.close() def test_lists_unread_messages_from_mailbox(self, sample_path, sample_mbox): self._add_message(sample_mbox, 'blaa-blaa-1', '') self._add_message(sample_mbox, 'blaa-blaa-2', 'O') self._add_message(sample_mbox, 'blaa-blaa-3', 'RO') sample_mbox.close() be = create_backend('mbox', name='sample', path=sample_path) be.open() try: msgs = list(be.list_messages()) folders = [folder for folder, msg in msgs] msg_ids = set(msg.get('message-id') for folder, msg in msgs) finally: be.close() assert len(msgs) == 2 assert all(folder == '' for folder in folders) assert msg_ids == set(['blaa-blaa-1', 'blaa-blaa-2']) def test_mbox_should_not_have_folders(self, sample_path): be = create_backend('mbox', path=sample_path) be.open() with pytest.raises(NotImplementedError): be.request_folders() def test_mbox_does_not_support_notifications(self, sample_path): be = create_backend('mbox', path=sample_path) be.open() with pytest.raises(NotImplementedError): be.notify_next_change() with pytest.raises(NotImplementedError): be.cancel_notifications() def test_open_should_fail_if_mailbox_does_not_exist(self, tmpdir): path = str(tmpdir.join('not-exist')) be = create_backend('mbox', path=path) with pytest.raises(IOError): be.open() def _add_message(self, mbox, msg_id, flags): m = mailbox.mboxMessage() m.set_payload('Hello world!', 'ascii') m.add_header('from', 'me@example.org') m.add_header('to', 'you@example.org') m.add_header('subject', 'Hi!') m.add_header('message-id', msg_id) m.set_flags(flags) mbox.lock() try: mbox.add(m) finally: mbox.unlock() class TestMaildir: """Tests for maildir backend.""" @pytest.fixture(autouse=True) def sample_maildir(self, sample_path): """Temporary maildir instance for tests.""" return mailbox.Maildir(sample_path, create=True) def test_create_maildir_backend(self): be = create_backend('maildir') assert be is not None def test_initially_mailbox_should_be_closed(self): be = create_backend('maildir') assert not be.is_open() def test_mailbox_should_be_open_when_opened(self, sample_path): be = create_backend('maildir', path=sample_path) be.open() assert be.is_open() def test_mailbox_should_be_closed(self, sample_path): be = create_backend('maildir', path=sample_path) be.open() be.close() assert not be.is_open() def test_lists_no_messages_from_empty_mailbox(self, sample_path): be = create_backend('maildir', name='sample', path=sample_path) be.open() try: msgs = list(be.list_messages()) assert len(msgs) == 0 finally: be.close() def test_lists_unread_messages_from_mailbox(self, sample_path, sample_maildir): self._add_message(sample_maildir, 'blaa-blaa-1', None, 'new') self._add_message(sample_maildir, 'blaa-blaa-2', None, 'cur') self._add_message(sample_maildir, 'blaa-blaa-3', 'S', 'cur') sample_maildir.close() be = create_backend('maildir', name='sample', path=sample_path) be.open() try: msgs = list(be.list_messages()) folders = [folder for folder, msg in msgs] msg_ids = set(msg.get('message-id') for folder, msg in msgs) finally: be.close() assert len(msgs) == 2 assert all(folder == '' for folder in folders) assert msg_ids == set(['blaa-blaa-1', 'blaa-blaa-2']) def test_lists_unread_messages_from_selected_folders(self, sample_path, sample_maildir): f = sample_maildir.add_folder('folder1') sample_maildir.add_folder('folder2') s = sample_maildir.add_folder('folder1.subfolder') self._add_message(sample_maildir, 'blaa-blaa-1', None, 'new') self._add_message(f, 'blaa-blaa-2', None, 'cur') self._add_message(s, 'blaa-blaa-3', None, 'cur') sample_maildir.close() be = create_backend('maildir', name='sample', path=sample_path, folders=['', 'folder1.subfolder']) be.open() try: msgs = list(be.list_messages()) folders = [folder for folder, msg in msgs] msg_ids = set(msg.get('message-id') for folder, msg in msgs) finally: be.close() assert len(msgs) == 2 assert set(['', 'folder1.subfolder']) == set(folders) assert msg_ids == set(['blaa-blaa-1', 'blaa-blaa-3']) def test_should_support_unicode_folder_names(self, sample_path, sample_maildir): """Python2 maildir folders must be str, not unicode. However folder in Mailnag configuration is represented as a json list, and json.loads converts strings to unicode. This test is here to ensure that unicode folder names work. Note: This is probably not needed with Python3. """ f = sample_maildir.add_folder('folder1') self._add_message(f, 'blaa-blaa-2', 'S', 'cur') sample_maildir.close() be = create_backend('maildir', name='sample', path=sample_path, folders=['', 'folder1']) be.open() try: msgs = list(be.list_messages()) finally: be.close() def test_folders_should_be_listed(self, sample_path, sample_maildir): sample_maildir.add_folder('folder1') sample_maildir.add_folder('folder2') sample_maildir.add_folder('folder1.subfolder') sample_maildir.close() be = create_backend('maildir', path=sample_path) be.open() folders = be.request_folders() assert set(['', 'folder1', 'folder2', 'folder1.subfolder']) == set(folders) def test_maildir_does_not_support_notifications(self, sample_path): be = create_backend('maildir', path=sample_path) be.open() with pytest.raises(NotImplementedError): be.notify_next_change() with pytest.raises(NotImplementedError): be.cancel_notifications() def test_open_should_fail_if_mailbox_does_not_exist(self, tmpdir): path = str(tmpdir.join('not-exist')) be = create_backend('maildir', path=path) with pytest.raises(IOError): be.open() def _add_message(self, maildir, msg_id, flags, subdir): m = mailbox.MaildirMessage() m.set_payload('Hello world!', 'ascii') m.add_header('from', 'me@example.org') m.add_header('to', 'you@example.org') m.add_header('subject', 'Hi!') m.add_header('message-id', msg_id) if flags: m.set_flags(flags) m.set_subdir(subdir) maildir.lock() try: maildir.add(m) finally: maildir.unlock() mailnag-2.2.0/tests/test_backends.py000066400000000000000000000047271401226772200174650ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # test_backends.py # # Copyright 2016 Timo Kankare # # 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. # """Test cases for backends.""" from Mailnag.backends import create_backend, get_mailbox_parameter_specs def test_create_imap_backend(): be = create_backend('imap', name='testing', user='nobody', password='', server='imap.example.org', port='', ssl=True, folders=['a', 'b']) assert be is not None assert not be.is_open() assert be.server == 'imap.example.org' def test_create_imap_backend_with_defaults(): be = create_backend('imap', name='testing') assert be is not None assert not be.is_open() assert be.server == '' def test_create_imap_backend_should_ignore_unknown_setting(): be = create_backend('imap', name='testing', odd='weird', weird='odd') assert be is not None def test_create_pop3_backend(): be = create_backend('pop3', name='testing', user='nobody', password='', server='pop.example.org', port='', ssl=True) assert be is not None assert not be.is_open() assert be.server == 'pop.example.org' def test_create_pop3_backend_with_defaults(): be = create_backend('pop3', name='testing') assert be is not None assert not be.is_open() assert be.server == '' def test_create_pop3_backend_should_ignore_unknown_setting(): be = create_backend('pop3', name='testing', odd='weird', weird='odd') assert be is not None def test_imap_backend_parameter_names(): specs = get_mailbox_parameter_specs('imap') names = [spec.param_name for spec in specs] assert set(['user', 'password', 'server', 'port', 'ssl', 'imap', 'idle', 'folders']) == set(names) def test_pop3_backend_parameter_names(): specs = get_mailbox_parameter_specs('pop3') names = [spec.param_name for spec in specs] assert set(['user', 'password', 'server', 'port', 'ssl', 'imap', 'idle']) == set(names) mailnag-2.2.0/tests/test_common_mutf7.py000066400000000000000000000026031401226772200203140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # test_account.py # # Copyright 2016 Timo Kankare # # 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. # """Test cases for mutf7.""" import pytest from Mailnag.common.mutf7 import encode_mutf7, decode_mutf7 def test_encode_mutf7(): expected = 'Die Katzen &- die M&AOQ-use' result = encode_mutf7('Die Katzen & die Mäuse') assert expected == result def test_decode_mutf7(): expected = 'Die Katzen & die Mäuse' result = decode_mutf7('Die Katzen &- die M&AOQ-use') assert expected == result def test_encode_mutf7_with_bytes_fails(): """Test to document current behaviour: encode_mutf7 requires unicode.""" with pytest.raises(Exception): encode_mutf7('Die Katzen & die Mäuse'.encode('utf-8')) mailnag-2.2.0/tests/test_mailsyncer.py000066400000000000000000000162401401226772200200520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # test_account.py # # Copyright 2018 Timo Kankare # # 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. # """Test cases for MailSyncer.""" import pytest import email from Mailnag.daemon.mails import MailSyncer from Mailnag.common.accounts import Account from Mailnag.backends.base import MailboxBackend class FakeBackend(MailboxBackend): """Fake mailbox backend implementation for testing.""" def __init__(self, **kw): self._opened = False self.messages = [] def open(self): self._opened = True def close(self): self._opened = False def is_open(self): return self._opened def list_messages(self): for msg in self.messages: yield "samplefolder", msg def request_folders(self): raise NotImplementedError("no folder support") def notify_next_change(self, callback=None, timeout=None): raise NotImplementedError("no notification support") def cancel_notifications(self): raise NotImplementedError("no notification support") class FakeAccount(Account): """Fake account implementation to use special test backend.""" def __init__(self, **kw): Account.__init__(self, **kw) self._backend = FakeBackend() def set_current_messages(self, messages): self._backend.messages = messages message_template = """\ From: You To: Me Subject: Hello... Message-ID: mid-{mid} Date: Tue, 01 May 2018 16:28:08 +0300 ...World! """ message_template_without_mid = """\ From: You To: Me Subject: Hello... Date: Tue, 01 May 2018 16:28:08 +0300 ...World! """ def make_messages(a, b): """Make list of messages with message ids in range(a, b).""" make_msg = email.message_from_string return [make_msg(message_template.format(mid=i)) for i in range(a, b)] def make_messages_without_mid(): """Make list of messages with message ids in range(a, b).""" return [message_template_without_mid] def test_no_mails_with_no_accounts(): syncer = MailSyncer([]) accounts = [] mails = syncer.sync(accounts) assert len(mails) == 0 def test_no_mails_in_an_account(): syncer = MailSyncer([]) account = FakeAccount() mails = syncer.sync([account]) assert len(mails) == 0 def test_mails_in_an_account(): syncer = MailSyncer([]) account = FakeAccount() messages = make_messages(0, 10) account.set_current_messages(messages) mails = syncer.sync([account]) assert len(mails) == 10 def test_syncing_account_multiple_times_should_not_affect(): syncer = MailSyncer([]) account = FakeAccount() messages = make_messages(0, 10) account.set_current_messages(messages) mails_in_first_sync = syncer.sync([account]) mails_in_second_sync = syncer.sync([account]) assert mails_in_first_sync == mails_in_second_sync def test_syncing_should_remove_read_messages(): syncer = MailSyncer([]) account = FakeAccount() messages = make_messages(0, 10) account.set_current_messages(messages) mails_in_first_sync = syncer.sync([account]) account.set_current_messages(messages[3:]) mails_in_second_sync = syncer.sync([account]) assert len(mails_in_second_sync) == 7 assert all(mail in mails_in_first_sync for mail in mails_in_second_sync) def test_syncing_should_add_new_messages(): syncer = MailSyncer([]) account = FakeAccount() messages = make_messages(0, 10) account.set_current_messages(messages) mails_in_first_sync = syncer.sync([account]) new_messages = make_messages(10, 14) account.set_current_messages(messages + new_messages) mails_in_second_sync = syncer.sync([account]) assert len(mails_in_second_sync) == 14 assert all(mail in mails_in_second_sync for mail in mails_in_first_sync) def test_syncing_should_update_messages(): syncer = MailSyncer([]) account = FakeAccount() messages = make_messages(0, 10) account.set_current_messages(messages) mails_in_first_sync = syncer.sync([account]) new_messages = make_messages(10, 14) account.set_current_messages(messages[3:] + new_messages) mails_in_second_sync = syncer.sync([account]) assert len(mails_in_second_sync) == 11 def test_syncing_without_account_should_keep_already_synced_messages(): syncer = MailSyncer([]) account = FakeAccount() messages = make_messages(0, 10) account.set_current_messages(messages) mails_in_first_sync = syncer.sync([account]) mails_in_second_sync = syncer.sync([]) assert mails_in_first_sync == mails_in_second_sync def test_syncing_multiple_accounts_should_collect_all_messages(): syncer = MailSyncer([]) account1 = FakeAccount() account2 = FakeAccount() account3 = FakeAccount() account1.set_current_messages(make_messages(0, 10)) account2.set_current_messages(make_messages(10, 20)) account3.set_current_messages(make_messages(20, 30)) mails = syncer.sync([account1, account2, account3]) assert len(mails) == 30 def test_syncing_multiple_account_separately_should_collect_all_messages(): syncer = MailSyncer([]) # Note: Accounts are identified by user, server and folders attributes, # see Account.get_id() account1 = FakeAccount(name='1', user='a') account2 = FakeAccount(name='2', server='b') account3 = FakeAccount(name='3') account1.set_current_messages(make_messages(0, 10)) account2.set_current_messages(make_messages(10, 20)) account3.set_current_messages(make_messages(20, 30)) syncer.sync([account1]) syncer.sync([account2]) mails = syncer.sync([account3]) assert len(mails) == 30 def test_syncing_same_mail_twice_from_same_account(): syncer = MailSyncer([]) account = FakeAccount() messages1 = make_messages(0, 1) messages2 = make_messages(0, 1) account.set_current_messages(messages1 + messages2) mails = syncer.sync([account]) assert len(mails) == 1 def test_syncing_same_mail_from_different_accounts(): syncer = MailSyncer([]) account1 = FakeAccount(name='1', user='a') account2 = FakeAccount(name='2', server='b') messages1 = make_messages(0, 1) messages2 = make_messages(0, 1) account1.set_current_messages(messages1) account2.set_current_messages(messages2) mails = syncer.sync([account1, account2]) # Note: Is this ok, or should it list message in both accounts? assert len(mails) == 1 def test_syncing_with_messages_without_message_ids(): syncer = MailSyncer([]) account1 = FakeAccount(name='1', user='a') account2 = FakeAccount(name='2', server='b') account3 = FakeAccount(name='3') messages1 = make_messages_without_mid() messages2 = make_messages_without_mid() messages3 = make_messages_without_mid() account1.set_current_messages(messages1) account2.set_current_messages(messages2) account3.set_current_messages(messages3) mails = syncer.sync([account1, account2, account3]) assert len(mails) == 3