pax_global_header 0000666 0000000 0000000 00000000064 13230747056 0014521 g ustar 00root root 0000000 0000000 52 comment=cb430f8b5c1699025517cddfabced910ea79a5bc
khard-0.12.2/ 0000775 0000000 0000000 00000000000 13230747056 0012674 5 ustar 00root root 0000000 0000000 khard-0.12.2/AUTHORS 0000664 0000000 0000000 00000000117 13230747056 0013743 0 ustar 00root root 0000000 0000000 Eric Scheibler - email [at] eric-scheibler [dot] de - http://eric-scheibler.de
khard-0.12.2/CHANGES 0000664 0000000 0000000 00000015626 13230747056 0013701 0 ustar 00root root 0000000 0000000 v0.1.0: 2014.09.18:
- initial release.
v0.2.0: 2014.10.01
- new project structure
- added twinkle plugin
- extended readme file
v0.2.1: 2015.01.14
- created pypi package
- missing attribute "fn" in VCard file is handled correctly now.
v0.2.2: 2015.03.14
- Added support for alot (MUA)
v0.3.0: 2015.03.25
- Added support for jabber, skype, twitter and webpage
- Created a filter for malformed vcard attributes
v0.3.1: 2015.03.26
- Nickname attribute added
- New parameter in config file: show_nicknames = yes / no
v0.3.2: 2015.03.27
- Read-only support for Categories attribute
v0.3.3: 2015.05.07
- twinkle option was renamed to phone: Now it prints all phone numbers like the mutt option does for
e-mail addresses (more general approach for external phone clients)
- adapted twinkle plugin for use with the new phone option
v0.4.0: 2015.06.15
- new option "add-email": Get full email from stdin, filter senders mail address and add that
address to an existing contact or create a new one. Feature tested with email client mutt.
v0.4.1: 2015.07.16
- improved search results for phone, mutt and alot
v0.5.0: 2015.09.05
- New option to merge two contacts
- Support for XDG_CONFIG_HOME parameter
- Post address: Added support for street and house number field with multiple lines
v0.6.0: 2015.09.20
- New options copy contact and move contact
- Changed behavior of merge command (have a look into the readme file for details)
- Get path to editor and merge editor from the $PATH variable
- Code cleanup: new object class AddressBook
v0.6.1: 2015.10.06
- Added title and role attribute
- Removed input restrictions of name and post address fields
- Parameter default_country in config file is not longer in use
v0.6.2: 2015.10.10
- Added completion function for zsh
v0.6.3: 2015.10.24
- Added note attribute
v0.7.0: 2015.12.18
- Support for vobject library version >= 0.8.2 from https://github.com/tBaxter/vobject
- Contact template syntax switched to yaml
- alot and mutt actions summarized to new email action (please have a look into the readme file for configuration changes)
- Support for extended name attributes
- Create and modify contact from stdin or from template file
- New action "export" to export data of existing contact in yaml format
- New argument --open-editor to open the preferred text editor after successful creation of new
contact from stdin or template file
- New argument {-u, --uid} to select contact by uid
- Added write support for categories attribute
- Added wrapper script for sdiff
- Fixed a bug, which prevented the creation of new contacts with the add-email action
v0.7.1: 2016.01.01
- Added support for multiple instances of one vcard attribute
v0.7.2: 2016.01.03
- Use of module atomicwrites to securely write vcards to disk
v0.7.3: 2016.01.08
- Cancel without further actions if the opened contacts editor is closed without saving (determined
by modification date of template file)
v0.7.4: 2016.01.11
- Fixed uid dictionary creation
v0.8.0: 2016.01.15
- Sort contact table by first or last name (take note of changed behaviour of "sort" option)
- New option -g, --group-by-addressbook to group contact table by address book
- Changes in config file:
- New group: contact table
- new option: sort to sort contact table by first or last name
- New option: group_by_addressbook to group contact table by address book
- Moved show_nicknames option from group "general" to group "contact table"
v0.8.1: 2016.01.16
- New option "show_uids" in config file to disable uid column in contact table
v0.9.0: 2016.03.17
- Fully restructured command line interface for better usability:
- general help with: khard -h
- help for a specific action: khard action -h
- Updated zsh completion function
- New Action addressbooks
- New option -p|--pretty for email and phone actions to get pretty formatted output
- Fix: Only delete contact after modify, copy or move action was completed successfully
v0.10.0: 2016.05.02
- New Action birthday: list birthdays, sorted by month and day
- option -p, --pretty was renamed to -p, --parsable: So, the pretty formatted email or
phone number table is the default now. Please adapt the configuration files of potential email and
phone applications (e.g.: mutt email -p %s)
v0.11.0: 2016.07.17
- Made khard python3 compatible (#59)
- Enhanced read and write support for vcard versions 3.0 and 4.0
- user input parser:
- Improved robustness and error handling
- Fixed org attribute parsing (#57)
- Support for private vcard extensions (#51)
- New action birthdays (#64)
- New options:
--display to display contacts by first or last name (#66)
--search-in-source-files to speed up program initialization (#75)
--skip-unparsable to skip unparsable vcard files (#75)
--strict-search to narrow the contact search to the name field
- Added some aliases for program actions (#65)
- Removed davcontroler module due to the python3 incompatibility (script moved into the misc folder)
- Updated zsh completion function and khards example config file
v0.11.1: 2016.07.31
- Workaround for the photo attribute parsing issue of vobject library 0.9.2 (#80)
- setup.py: try to convert readme file into restructured text format (pypi requirement) and specify encoding explicitly (#83)
v0.11.2: 2016.08.31
- Extended the photo parsing workaround from khard version 0.11.1 to all base64 encoded vcard attributes (#86 and #87)
- Show additional/middle names in name column of contact table (#89)
- Added khard-runner.py helper script to simplify source code launching
v0.11.3: 2016.09.20
- Pinned version of vcard library vobject to version 0.9.2 due to bug https://github.com/eventable/vobject/issues/39
- Added some new action aliases
- Fix for birthday date processing (#95)
v0.11.4: 2017.02.16
- Unpinned vobject library version (base64 decoding/encoding bug was fixed upstream)
- New option: -c / --config /path/to/config.file
- Changed short option of --search-in-source-files from -c to -f to avoid confusion with the new -c / --config option
- Minor bug fixes
v0.12.0: 2018.01.12
- Vcard: Support for anniversary attribute (#138)
- Config: New config parameter: localize_dates (#118)
- Action list: -p / --parsable prints a tab separated contact list (#141)
- Action remove: --force: Remove contact without confirmation (#140)
- Mutt: You have to update the query command in mutt's config file: set query_command= "khard email --parsable '%s'"
- Minor changes in khard example config, zsh completion function and twinkle scripts
- Fix: Results of phone number search improved
- Fix: Yaml parser switched from pyyaml to ruamel.yaml to allow special unicode characters in the contact template (#133)
- Fix: Accentuated characters sorted wrong in contact list (#127)
v0.12.1: 2018.01.14
- Fix for issue #148: Config variable "sort" not longer mandatory in config file
v0.12.2: 2018.01.21
- Fixed: Found contact twice when the whole uid was used with -u (#161)
- Fixed: A minor bug in the contact search function (#160)
khard-0.12.2/LICENSE 0000664 0000000 0000000 00000104513 13230747056 0013705 0 ustar 00root root 0000000 0000000 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. 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
them 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 prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. 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.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey 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;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If 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 convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU 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 that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
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.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
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.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
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
state 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 3 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, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program 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, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU 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. But first, please read
.
khard-0.12.2/MANIFEST.in 0000664 0000000 0000000 00000000164 13230747056 0014433 0 ustar 00root root 0000000 0000000 include AUTHORS
include CHANGES
include LICENSE
include README.md
recursive-include misc *
recursive-include test *
khard-0.12.2/README.md 0000664 0000000 0000000 00000032205 13230747056 0014155 0 ustar 00root root 0000000 0000000 khard
=====
Khard is an address book for the Linux console. It creates, reads, modifies and removes carddav
address book entries at your local machine. Khard is also compatible to the email clients mutt and
alot and the SIP client twinkle. You can find more information about khard and the whole
synchronization process
[here](http://eric-scheibler.de/en/blog/2014/10/Sync-calendars-and-address-books-between-Linux-and-Android/).
Khard is developed and tested on Debian operating system, versions 7, 8 and testing but should run
on all Unix-like systems.
If you encounter bugs, please contact me via email: email (at) eric-scheibler (dot) de.
Warning: If you want to create or modify contacts with khard, beware that the vcard standard is very
inconsistent and lacks interoperability. Different actors in that sector have defined their own
extensions and even produce non-standard output. A good example is the type value, which is tied to
phone numbers, email and post addresses. Khard tries to avoid such incompatibilities but if you sync
your contacts with an Android or iOS device, expect problems. You are on the save side, if you only
use khard to read contacts. For further information about the vcard compatibility issues have a look
into [this blog post](http://alessandrorossini.org/2012/11/15/the-sad-story-of-the-vcard-format-and-its-lack-of-interoperability/).
With version 0.11.0, khard changed from python2 to python3. So if you come from a prior khard
version, it may be necessary to reinstall in a newly created python3 virtual environment.
Prerequisites
-------------
You have to install and configure a caldav and carddav server. I recommend
[Baïkal](http://baikal-server.com).
Then you must synchronize the calendars and address books to your local machine with
[vdirsyncer](https://github.com/untitaker/vdirsyncer).
And you need pip to install python modules:
```
sudo aptitude install python-setuptools
sudo easy_install pip
```
Installation
------------
### From pypi ###
Khard is installable via pip. I recommend virtualenv to create a separate python3 environment. So
your system stays clean. Additionally you don't have to struggle with different python instances,
especially if your operating system still defaults to python2.
```
# install virtualenv package
sudo pip install virtualenv
# create folder for all virtualenv's and put ~/.virtualenvs/bin in your shell's executable path
mkdir ~/.virtualenvs
# create new python3 virtual environment with the name "khard"
virtualenv -p python3 ~/.virtualenvs/khard
# to install khard, use the pip command from that newly created environment
# otherwise it would be installed in the users home directory
~/.virtualenvs/khard/bin/pip install khard
# create subfolder for symlinks of local binaries
# and don't forget to add it to your shell's executable path too
mkdir ~/.virtualenvs/bin
# create a symlink to the local binary folder
ln -s ~/.virtualenvs/khard/bin/khard ~/.virtualenvs/bin
```
More information about virtualenv at http://docs.python-guide.org/en/latest/dev/virtualenvs/
### From source ###
If you instead want to run the source code directly, you may install required python modules by
hand, clone from git and run the khard-runner.py script:
~~~
pip install --user atomicwrites configobj ruamel.yaml unidecode vobject
git clone https://github.com/scheibler/khard.git
cd khard/
./khard-runner.py [action [options]]
~~~
### Configuration ###
To get the example config file and the other extra data, you can clone from git (see above) or
download package from pypi:
```
pip install --download /tmp --no-deps --no-use-wheel khard
tar xfz /tmp/khard-x.x.x.tar.gz
rm /tmp/khard-x.x.x.tar.gz
cd khard-x.x.x/
```
Now copy the example config file and adapt it's contents to your needs:
```
mkdir ~/.config/khard/
cp misc/khard/khard.conf.example ~/.config/khard/khard.conf
```
### Davcontroller ###
Khard also contains a helper script called davcontroller. It's designed to create and remove address
books and calendars at the server. I have created davcontroller cause my previously used CalDAV
server (Darwin calendarserver) offered no simple way to create new address books and calendars. But
davcontroller should be considered as a hacky solution and it's only tested against the Darwin
calendarserver. So if your CalDAV server offers a way to create new address books and calendars I
recommend to prefer that method over davcontroller.
If you nonetheless want to try davcontroller, you have to install the CalDAVClientLibrary first.
Unfortunately that library isn't compatible to python3 so you have to create an extra python2
virtual environment and install in there:
```
# create python2 virtual environment
virtualenv -p python2 ~/.virtualenvs/davcontroller
# get library from svn repository
sudo aptitude install subversion
svn checkout http://svn.calendarserver.org/repository/calendarserver/CalDAVClientLibrary/trunk CalDAVClientLibrary
cd CalDAVClientLibrary
# install library
~/.virtualenvs/davcontroller/bin/python setup.py install
# start davcontroller script
~/.virtualenvs/davcontroller/bin/python /path/to/khard-x.x.x/misc/davcontroller/davcontroller.py
```
Usage
-----
The following subsections give an overview of khard's main features.
You may get general help and all available actions with
```
khard --help
```
If you need help on a specific action, use:
```
khard action --help
```
Beware, that the order of the command line parameters matters.
### Show contacts ###
After you have created a new address book or calendar and you have synced it to your local machine,
you can list all available contacts with the following command:
```
khard list
```
or if you have more than one address book and you want to filter the output:
```
khard list -a addressbook1,addressbook2
```
The resulting contact table only contains the first phone number and email address. If you want to view all contact
details you can pick one from the list:
```
khard details
```
or search for it:
```
khard details [--strict-search] name of contact
```
or select the contact by it's uid, which you can find at the contacts table:
```
khard details -u ID
```
The parameters -a and -u from the examples above are always optional. If you don't use them or your
input produces unambiguous results, you may pick the contacts from a list instead.
The search parameter searches in all data fields. Therefore you aren't limited to the contact's name
but you also could for example search for a part of a phone number, email address or post address.
However if you explicitly want to narrow your search to the name field, you may use the
--strict-search parameter instead.
### Create contact ###
Add new contact with the following command:
```
khard new [-a "address book name"]
```
The template for the new contact opens in the text editor, which you can set in the khard.conf file.
It follows the yaml syntax.
Alternatively you can create the contact from stdin:
```
echo """
First name : John
Last name : Smith
Email :
work : john.smith@example.org
Phone :
home : xxx 555 1234
Categories :
- cat1
- cat2
- cat3
""" | khard new [-a "address book name"]
```
or create from input template file:
```
khard new -i contact.yaml [-a "address book name"]
```
You may get an empty contact template with the following command:
```
khard export --empty-contact-template -o empty.yaml
```
Per default khard creates vcards of version 3.0. If your other contact applications support vcards
of the more recent version 4.0, you may change this with the option --vcard-version. Example:
```
khard new --vcard-version=4.0 [-a "address book name"]
```
For a more permanent solution you may set the preferred_version parameter in the vcard section of
the khard config file (see misc/khard/khard.conf.example for more details). But beware, that khard
cannot convert already existing contacts from version 3.0 to 4.0. Therefore this setting is not
applicable to the modify action.
### Edit contacts ###
Use the following to modify the contact after successful creation:
```
khard modify [-a addr_name] [-u uid|search terms [search terms ...]]
```
If you want to edit the contact elsewhere, you can export the filled contact template:
```
khard export -o contact.yaml [-a addr_name] [-u uid|search terms [search terms ...]]
```
Edit the yaml file and re-import either through stdin:
```
cat contact.yaml | khard modify [-a addr_name] [-u uid|search terms [search terms ...]]
```
or file name:
```
khard modify -i contact.yaml [-a addr_name] [-u uid|search terms [search terms ...]]
```
If you want to merge contacts use the following to select a first and then a second contact:
```
khard merge [-a source_abook] [-u uid|search terms [search terms ...]] [-A target_abook] [-U target_uid|-t target_search_terms]
```
You will be launched into your merge_editor ( see the "merge_editor" option in khard.conf)
where you can merge all changes from the first selected contact onto the second.
Once you are finished, the first contact is deleted and the second one updated.
Copy or move contact:
```
khard copy [-a source_abook] [-u uid|search terms [search terms ...]] [-A target_abook]
khard move [-a source_abook] [-u uid|search terms [search terms ...]] [-A target_abook]
```
Remove contact:
```
khard remove [-a addr_name] [-u uid|search terms [search terms ...]]
```
davcontroller
-------------
This small script helps to create and remove new address books and calendars at the carddav and
caldav server.
List available resources:
```
davcontroller -H example.com -p 11111 -u USERNAME -P PASSWORD list
```
Possible actions are: list, new-addressbook, new-calendar and remove. After creating or removing you
must adapt your vdirsyncer config.
mutt
----
Khard may be used as an external address book for the email client mutt. To accomplish that, add the
following to your mutt config file (mostly ~/.mutt/muttrc):
```
set query_command= "khard email --parsable '%s'"
bind editor complete-query
bind editor ^T complete
```
Then you can complete email addresses by pressing the Tab-key in mutt's new mail dialog. If your
address books contain hundreds or even thousands of contacts and the query process is very slow, you
may try the --search-in-source-files option to speed up the search:
```
set query_command= "khard email --parsable --search-in-source-files '%s'"
```
To add email addresses to khard's address book, you may also add the following lines to your muttrc file:
```
macro index,pager A \
"khard add-email" \
"add the sender email address to khard"
```
Then navigate to an email message in mutt's index view and press "A" to start the address import dialog.
Alot
----
Add the following lines to your alot config file:
```
[accounts]
[[youraccount]]
[[[abook]]]
type = shellcommand
command = khard email --parsable
regexp = '^(?P[^@]+@[^\t]+)\t+(?P[^\t]+)'
ignorecase = True
```
Twinkle
-------
For those who also use the SIP client twinkle to take phone calls, khard can be used to query
incoming numbers. The plugin tries to find the incoming caller id and speaks it together with the
phone's ring tone. The plugin needs the following programs:
```
sudo aptitude install ffmpeg espeak sox mpc
```
sox and ffmpeg are used to cut and convert the new ring tone and espeak speaks the caller id. mpc is a client
for the music player daemon (mpd). It's required to stop music during an incoming call. Skip the last,
if you don't use mpd. Don't forget to set the "stop_music"-parameter in the config.py file to
False too.
After the installation, copy the scripts and sounds folders to your twinkle config folder:
```
cp -R misc/twinkle/* ~/.twinkle/
```
Then edit your twinkle config file (mostly ~/.twinkle/twinkle.cfg) like this:
```
# RING TONES
# We need a default ring tone. Otherwise the phone would not ring at all, if something with the
# custom ring tone creation goes wrong.
ringtone_file=/home/USERNAME/.twinkle/sounds/incoming_call.wav
ringback_file=/home/USERNAME/.twinkle/sounds/outgoing_call.wav
# SCRIPTS
script_incoming_call=/home/USERNAME/.twinkle/scripts/incoming_call.py
script_in_call_answered=
script_in_call_failed=/home/USERNAME/.twinkle/scripts/incoming_call_failed.py
script_outgoing_call=
script_out_call_answered=
script_out_call_failed=
script_local_release=/home/USERNAME/.twinkle/scripts/incoming_call_ended.py
script_remote_release=/home/USERNAME/.twinkle/scripts/incoming_call_ended.py
```
Zsh
---
The file misc/zsh/_khard contains a zsh completion definition for khard.
Install by copying to a directory where zsh searches for completion functions (the $fpath array).
If you, for example, put all completion functions into the folder ~/.zsh/completions you must add
the following to your zsh main config file:
```
fpath=( $HOME/.zsh/completions $fpath )
autoload -U compinit
compinit
```
sdiff
-----
Use the wrapper script misc/sdiff/sdiff_khard_wrapper.sh if you want to use sdiff as your contact
merging tool. Just make the script executable and set it as your merge editor in khard's config file:
```
merge_editor = /path/to/sdiff_khard_wrapper.sh
```
Related projects
----------------
If you need a console based calendar too, try out [khal](https://github.com/geier/khal).
khard-0.12.2/khard-runner.py 0000775 0000000 0000000 00000000163 13230747056 0015651 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
from khard.khard import main
if __name__ == '__main__':
main()
khard-0.12.2/khard/ 0000775 0000000 0000000 00000000000 13230747056 0013765 5 ustar 00root root 0000000 0000000 khard-0.12.2/khard/__init__.py 0000664 0000000 0000000 00000000000 13230747056 0016064 0 ustar 00root root 0000000 0000000 khard-0.12.2/khard/__main__.py 0000664 0000000 0000000 00000000070 13230747056 0016054 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from .khard import main
main()
khard-0.12.2/khard/actions.py 0000664 0000000 0000000 00000004173 13230747056 0016004 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
class Actions:
"""A class to manage the names and aliases of the command line
subcommands."""
action_map = {
"add-email": [],
"addressbooks": ["abooks"],
"birthdays": ["bdays"],
"copy": ["cp"],
"details": ["show"],
"email": [],
"export": [],
"list": ["ls"],
"merge": [],
"modify": ["edit", "ed"],
"move": ["mv"],
"new": ["add"],
"phone": [],
"remove": ["delete", "del", "rm"],
"source": ["src"]
}
@classmethod
def get_action_for_alias(cls, alias):
"""Find the name of the action for the supplied alias. If no action is
asociated with the given alias, None is returned.
:param alias: the alias to look up
:type alias: str
:rturns: the name of the corresponding action or None
:rtype: str or NoneType
"""
for action, alias_list in cls.action_map.items():
if alias in alias_list:
return action
return None
@classmethod
def get_alias_list_for_action(cls, action):
"""Find all aliases for the given action. If there is no such action,
None is returned.
:param action: the action name to look up
:type action: str
:returns: the list of aliases corresponding to the action or None
:rtype: list(str) or NoneType
"""
return cls.action_map.get(action)
@classmethod
def get_list_of_all_actions(cls):
"""Find the names of all defined actions.
:returns: all action names
:rtype: iterable(str)
"""
return cls.action_map.keys()
@classmethod
def get_all_actions_and_aliases(cls):
"""Find the names of all defined actions and their aliases.
:returns: the names of all actions and aliases
:rtype: list(str)
"""
all = []
for key, value in cls.action_map.items():
all.append(key)
all.extend(value)
return all
khard-0.12.2/khard/address_book.py 0000664 0000000 0000000 00000015674 13230747056 0017013 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""A simple class to load and manage the vcard files from disk."""
import glob
import logging
import os
import re
from .carddav_object import CarddavObject
class AddressBook:
"""Holds the contacts inside one address book folder. On disk they are
stored in vcard files."""
def __init__(self, name, path):
self.loaded = False
self.contact_list = []
self._uids = set()
self.name = name
self.path = os.path.expanduser(path)
if not os.path.isdir(self.path):
raise FileNotFoundError(
"[Errno 2] The path %s to the address book %s does not exist."
% (self.path, self.name))
def __str__(self):
return self.name
def __eq__(self, other):
return isinstance(other, AddressBook) and self.name == other.name
def __ne__(self, other):
return not self == other
def _find_vcard_files(self, search=None):
"""Find all vcard files inside this address book. If a search string
is given only files which contents match that will be returned.
:param search: a regular expression to limit the results
:type search: str
:returns: the paths of the vcard files
:rtype: generator
"""
for filename in glob.glob(os.path.join(self.path, "*.vcf")):
if search:
with open(filename, "r") as filehandle:
if re.search(search, filehandle.read(),
re.IGNORECASE | re.DOTALL):
yield filename
else:
yield filename
def _check_uids(self):
"""Check that the uids of all cards are unique across this address
book.
:returns: the set of duplicate uids
:rtype: set(str)
"""
duplicates = set()
for contact in self.contact_list:
uid = contact.get_uid()
if uid in self._uids:
duplicates.add(uid)
else:
self._uids.add(uid)
return duplicates
def load_all_vcards(self, private_objects=tuple(), localize_dates=True,
search=None):
"""Load all vcard files in this address book from disk. If a search
string is given only files which contents match that will be loaded.
:param private_objects: the names of private vcard extension fields to
load
:type private_objects: list(str) or tuple(str)
:param search: a regular expression to limit the results
:type search: str
:returns: the number of successfully loaded cards and the number of
errors
:rtype: int, int
"""
if self.loaded:
return len(self.contact_list), 0
contacts = 0
errors = 0
for filename in self._find_vcard_files(search=search):
contacts += 1
try:
card = CarddavObject.from_file(self, filename,
private_objects, localize_dates)
except IOError as err:
logging.debug("Error: Could not open file %s\n%s", filename,
err)
errors += 1
except Exception as err:
logging.debug("Error: Could not parse file %s\n%s", filename,
err)
errors += 1
else:
self.contact_list.append(card)
duplicates = self._check_uids()
if duplicates:
logging.warning(
"There are duplicate UIDs in the address book %s: %s",
self.name, duplicates)
self.loaded = True
return contacts, errors
def _search_all(self, query):
"""Search in all fields for contacts matching query.
:param query: the query to search for
:type query: str
:yields: all found contacts
:rtype: generator(carddav_object.CarddavObject)
"""
regexp = re.compile(query, re.IGNORECASE | re.DOTALL)
for contact in self.contact_list:
# search in all contact fields
contact_details = contact.print_vcard()
if regexp.search(contact_details) is not None:
yield contact
else:
# find phone numbers with special chars like /
contact_details_without_special_chars = re.sub("[^a-zA-Z0-9\n]",
"", contact_details)
if regexp.search(contact_details_without_special_chars) is not None \
and len(re.sub("\D", "", query)) >= 3:
yield contact
def _search_names(self, query):
"""Search in the name filed for contacts matching query.
:param query: the query to search for
:type query: str
:yields: all found contacts
:rtype: generator(carddav_object.CarddavObject)
"""
regexp = re.compile(query, re.IGNORECASE | re.DOTALL)
for contact in self.contact_list:
# only search in contact name
if regexp.search(contact.get_full_name()) is not None:
yield contact
def _search_uid(self, query):
"""Search for contacts with a matching uid.
:param query: the query to search for
:type query: str
:yields: all found contacts
:rtype: generator(carddav_object.CarddavObject)
"""
found = False
# Search for contacts with uid == query.
for contact in self.contact_list:
if contact.get_uid() == query:
found = True
yield contact
# If that fails, search for contacts where uid starts with query.
if not found:
for contact in self.contact_list:
if contact.get_uid().startswith(query):
yield contact
def search(self, query, method="all"):
"""Search this address book for contacts matching the query. The
method can be one of "all", "name" and "uid".
:param query: the query to search for
:type query: str
:param method: the type of fileds to use when seaching
:type method: str
:returns: all found contacts
:rtype: list(carddav_object.CarddavObject)
"""
if method == "all" or method == "name":
if method == "all":
search_function = self._search_all
else:
search_function = self._search_names
# check if query is already an regexp, else escape it
if not (query.startswith(".*") and query.endswith(".*")):
query = re.escape(query)
return list(search_function(query))
elif method == "uid":
# always escape uids
return list(self._search_uid(re.escape(query)))
else:
raise ValueError('Only the search methods "all", "name" and "uid" '
'are supported.')
khard-0.12.2/khard/carddav_object.py 0000664 0000000 0000000 00000215052 13230747056 0017276 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# contact object class
# vcard version 3.0: https://tools.ietf.org/html/rfc2426
# vcard version 4.0: https://tools.ietf.org/html/rfc6350
import datetime
import locale
import os
import re
import sys
from atomicwrites import atomic_write
import vobject
import ruamel.yaml
from ruamel.yaml import YAML
from . import helpers
from .object_type import ObjectType
class CarddavObject:
# vcard v3.0 supports the following type values
phone_types_v3 = ("bbs", "car", "cell", "fax", "home", "isdn", "msg",
"modem", "pager", "pcs", "video", "voice", "work")
email_types_v3 = ("home", "internet", "work", "x400")
address_types_v3 = ("dom", "intl", "home", "parcel", "postal", "work")
# vcard v4.0 supports the following type values
phone_types_v4 = ("text", "voice", "fax", "cell", "video", "pager",
"textphone", "home", "work")
email_types_v4 = ("home", "internet", "work")
address_types_v4 = ("home", "work")
def __init__(self, address_book, filename, supported_private_objects,
vcard_version, localize_dates):
"""Initialize the vcard object.
:param address_book: a reference to the address book where this vcard
is stored
:type address_book: khard.address_book.AddressBook
:param filename: the path to the file where this vcard is stored or
None
:type filename: str or NoneType
:param supported_private_objects: the list of private property names
that will be loaded from the actual vcard and represented in this
pobject
:type supported_private_objects: list(str)
:param vcard_version: str or None
:type vcard_version: str
:param localize_dates: should the formatted output of anniversary and
birthday be localized or should the isoformat be used instead
:type localize_dates: bool
"""
self.vcard = None
self.address_book = address_book
self.filename = filename
self.supported_private_objects = supported_private_objects
self.localize_dates = localize_dates
# load vcard
if self.filename is None:
# create new vcard object
self.vcard = vobject.vCard()
# add uid
self.add_uid(helpers.get_random_uid())
# use uid for vcard filename
self.filename = os.path.join(address_book.path,
self.get_uid() + ".vcf")
# add preferred vcard version
self._add_version(vcard_version)
else:
# create vcard from .vcf file
with open(self.filename, "r") as file:
contents = file.read()
# create vcard object
try:
self.vcard = vobject.readOne(contents)
except Exception:
# if creation fails, try to repair some vcard attributes
self.vcard = vobject.readOne(self._filter_invalid_tags(contents))
#######################################
# factory methods to create new contact
#######################################
@classmethod
def new_contact(cls, address_book, supported_private_objects, version,
localize_dates):
"""Use this to create a new and empty contact."""
return cls(address_book, None, supported_private_objects, version,
localize_dates)
@classmethod
def from_file(cls, address_book, filename, supported_private_objects,
localize_dates):
"""
Use this if you want to create a new contact from an existing .vcf
file.
"""
return cls(address_book, filename, supported_private_objects, None,
localize_dates)
@classmethod
def from_user_input(cls, address_book, user_input,
supported_private_objects, version, localize_dates):
"""Use this if you want to create a new contact from user input."""
contact = cls(address_book, None, supported_private_objects, version,
localize_dates)
contact._process_user_input(user_input)
return contact
@classmethod
def from_existing_contact_with_new_user_input(cls, contact, user_input,
localize_dates):
"""
Use this if you want to clone an existing contact and replace its data
with new user input in one step.
"""
contact = cls(contact.address_book, contact.filename,
contact.supported_private_objects, None, localize_dates)
contact._process_user_input(user_input)
return contact
######################################
# overwrite some default class methods
######################################
def __str__(self):
return self.get_full_name()
def __eq__(self, other):
return isinstance(other, CarddavObject) and \
self.print_vcard(show_address_book=False, show_uid=False) == \
other.print_vcard(show_address_book=False, show_uid=False)
def __ne__(self, other):
return not self == other
#####################
# getters and setters
#####################
def _get_rev(self):
"""
:rtype: str
"""
try:
return self.vcard.rev.value
except AttributeError:
return ""
def _add_rev(self, dt):
rev_obj = self.vcard.add('rev')
rev_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2dZ" % (
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
def get_uid(self):
"""
:rtype: str
"""
try:
return self.vcard.uid.value
except AttributeError:
return ""
def add_uid(self, uid):
uid_obj = self.vcard.add('uid')
uid_obj.value = helpers.convert_to_vcard(
"uid", uid, ObjectType.string)
def get_version(self):
"""
:rtype: str
"""
try:
return self.vcard.version.value
except AttributeError:
return ""
def _add_version(self, vcard_version):
version_obj = self.vcard.add('version')
version_obj.value = helpers.convert_to_vcard(
"version", vcard_version, ObjectType.string)
def _get_name_prefixes(self):
"""
:rtype: list(str)
"""
try:
prefix_list = self.vcard.n.value.prefix
except AttributeError:
prefix_list = []
else:
# check if list only contains an empty string ([""])
if not ''.join(prefix_list):
prefix_list = []
return prefix_list if isinstance(prefix_list, list) else [prefix_list]
def _get_first_names(self):
"""
:rtype: list(str)
"""
try:
first_name_list = self.vcard.n.value.given
except AttributeError:
first_name_list = []
else:
# check if list only contains an empty string ([""])
if not ''.join(first_name_list):
first_name_list = []
return first_name_list if isinstance(first_name_list, list) \
else [first_name_list]
def _get_additional_names(self):
"""
:rtype: list(str)
"""
try:
additional_name_list = self.vcard.n.value.additional
except AttributeError:
additional_name_list = []
else:
# check if list only contains an empty string ([""])
if not ''.join(additional_name_list):
additional_name_list = []
return additional_name_list if isinstance(additional_name_list, list) \
else [additional_name_list]
def _get_last_names(self):
"""
:rtype: list(str)
"""
try:
last_name_list = self.vcard.n.value.family
except AttributeError:
last_name_list = []
else:
# check if list only contains an empty string ([""])
if not ''.join(last_name_list):
last_name_list = []
return last_name_list if isinstance(last_name_list, list) \
else [last_name_list]
def _get_name_suffixes(self):
"""
:rtype: list(str)
"""
try:
suffix_list = self.vcard.n.value.suffix
except AttributeError:
suffix_list = []
else:
# check if list only contains an empty string ([""])
if not ''.join(suffix_list):
suffix_list = []
return suffix_list if isinstance(suffix_list, list) else [suffix_list]
def get_full_name(self):
"""
:rtype: str
"""
try:
return self.vcard.fn.value
except AttributeError:
return ""
def get_first_name_last_name(self):
"""
:rtype: str
"""
names = []
if self._get_first_names():
names += self._get_first_names()
if self._get_additional_names():
names += self._get_additional_names()
if self._get_last_names():
names += self._get_last_names()
if names:
return helpers.list_to_string(names, " ")
else:
return self.get_full_name()
def get_last_name_first_name(self):
"""
:rtype: str
"""
last_names = []
if self._get_last_names():
last_names += self._get_last_names()
first_and_additional_names = []
if self._get_first_names():
first_and_additional_names += self._get_first_names()
if self._get_additional_names():
first_and_additional_names += self._get_additional_names()
if last_names and first_and_additional_names:
return "%s, %s" % (
helpers.list_to_string(last_names, " "),
helpers.list_to_string(first_and_additional_names, " "))
elif last_names:
return helpers.list_to_string(last_names, " ")
elif first_and_additional_names:
return helpers.list_to_string(first_and_additional_names, " ")
else:
return self.get_full_name()
def _add_name(self, prefix, first_name, additional_name, last_name,
suffix):
# n
name_obj = self.vcard.add('n')
name_obj.value = vobject.vcard.Name(
prefix=helpers.convert_to_vcard(
"name prefix", prefix, ObjectType.string_or_list_with_strings),
given=helpers.convert_to_vcard(
"first name", first_name,
ObjectType.string_or_list_with_strings),
additional=helpers.convert_to_vcard(
"additional name", additional_name,
ObjectType.string_or_list_with_strings),
family=helpers.convert_to_vcard(
"last name", last_name,
ObjectType.string_or_list_with_strings),
suffix=helpers.convert_to_vcard(
"name suffix", suffix, ObjectType.string_or_list_with_strings))
# fn
if not self.vcard.getChildValue("fn") and (self._get_first_names() or
self._get_last_names()):
names = []
if self._get_name_prefixes():
names += self._get_name_prefixes()
if self._get_first_names():
names += self._get_first_names()
if self._get_last_names():
names += self._get_last_names()
if self._get_name_suffixes():
names += self._get_name_suffixes()
name_obj = self.vcard.add('fn')
name_obj.value = helpers.list_to_string(names, " ")
def _get_organisations(self):
"""
:returns: list of organisations, sorted alphabetically
:rtype: list(list(str))
"""
organisations = []
for child in self.vcard.getChildren():
if child.name == "ORG":
organisations.append(child.value)
return sorted(organisations)
def _add_organisation(self, organisation):
org_obj = self.vcard.add('org')
org_obj.value = helpers.convert_to_vcard(
"organisation", organisation, ObjectType.list_with_strings)
# check if fn attribute is already present
if not self.vcard.getChildValue("fn") \
and self._get_organisations():
# if not, set fn to organisation name
org_value = helpers.list_to_string(self._get_organisations()[0],
", ")
name_obj = self.vcard.add('fn')
name_obj.value = org_value.replace("\n", " ").replace("\\", "")
showas_obj = self.vcard.add('x-abshowas')
showas_obj.value = "COMPANY"
def _get_titles(self):
"""
:rtype: list(list(str))
"""
titles = []
for child in self.vcard.getChildren():
if child.name == "TITLE":
titles.append(child.value)
return sorted(titles)
def _add_title(self, title):
title_obj = self.vcard.add('title')
title_obj.value = helpers.convert_to_vcard("title", title,
ObjectType.string)
def _get_roles(self):
"""
:rtype: list(list(str))
"""
roles = []
for child in self.vcard.getChildren():
if child.name == "ROLE":
roles.append(child.value)
return sorted(roles)
def _add_role(self, role):
role_obj = self.vcard.add('role')
role_obj.value = helpers.convert_to_vcard("role", role,
ObjectType.string)
def get_phone_numbers(self):
"""
: returns: dict of type and phone number list
:rtype: dict(str, list(str))
"""
phone_dict = {}
for child in self.vcard.getChildren():
if child.name == "TEL":
type = helpers.list_to_string(
self._get_types_for_vcard_object(child, "voice"), ", ")
if phone_dict.get(type) is None:
phone_dict[type] = []
try:
# detect uri
if child.params.get("VALUE")[0] == "uri":
phone_dict[type].append(child.value.split(":")[1])
except (IndexError, TypeError):
phone_dict[type].append(child.value)
# sort phone number lists
for number_list in phone_dict.values():
number_list.sort()
return phone_dict
def _add_phone_number(self, type, number):
standard_types, custom_types, pref = self._parse_type_value(
helpers.string_to_list(type, ","), number, self.phone_types_v4 if
self.get_version() == "4.0" else self.phone_types_v3)
if not standard_types and not custom_types and pref == 0:
raise ValueError("Error: label for phone number " + number +
" is missing.")
elif len(custom_types) > 1:
raise ValueError("Error: phone number " + number + " got more "
"than one custom label: " +
helpers.list_to_string(custom_types, ", "))
else:
phone_obj = self.vcard.add('tel')
if self.get_version() == "4.0":
phone_obj.value = "tel:%s" % helpers.convert_to_vcard(
"phone number", number, ObjectType.string)
phone_obj.params['VALUE'] = ["uri"]
if pref > 0:
phone_obj.params['PREF'] = str(pref)
else:
phone_obj.value = helpers.convert_to_vcard(
"phone number", number, ObjectType.string)
if pref > 0:
standard_types.append("pref")
if standard_types:
phone_obj.params['TYPE'] = standard_types
if custom_types:
number_of_custom_phone_number_labels = 0
for label in self.vcard.getChildren():
if label.name == "X-ABLABEL" \
and label.group.startswith("itemtel"):
number_of_custom_phone_number_labels += 1
group_name = "itemtel%d" % (
number_of_custom_phone_number_labels+1)
phone_obj.group = group_name
label_obj = self.vcard.add('x-ablabel')
label_obj.group = group_name
label_obj.value = custom_types[0]
def get_email_addresses(self):
"""
: returns: dict of type and email address list
:rtype: dict(str, list(str))
"""
email_dict = {}
for child in self.vcard.getChildren():
if child.name == "EMAIL":
type = helpers.list_to_string(
self._get_types_for_vcard_object(child, "internet"), ", ")
if email_dict.get(type) is None:
email_dict[type] = []
email_dict[type].append(child.value)
# sort email address lists
for email_list in email_dict.values():
email_list.sort()
return email_dict
def add_email_address(self, type, address):
standard_types, custom_types, pref = self._parse_type_value(
helpers.string_to_list(type, ","), address, self.email_types_v4 if
self.get_version() == "4.0" else self.email_types_v3)
if not standard_types and not custom_types and pref == 0:
raise ValueError("Error: label for email address " + address +
" is missing.")
elif len(custom_types) > 1:
raise ValueError("Error: email address " + address + " got more "
"than one custom label: " +
helpers.list_to_string(custom_types, ", "))
else:
email_obj = self.vcard.add('email')
email_obj.value = helpers.convert_to_vcard(
"email address", address, ObjectType.string)
if self.get_version() == "4.0":
if pref > 0:
email_obj.params['PREF'] = str(pref)
else:
if pref > 0:
standard_types.append("pref")
if standard_types:
email_obj.params['TYPE'] = standard_types
if custom_types:
number_of_custom_email_labels = 0
for label in self.vcard.getChildren():
if label.name == "X-ABLABEL" \
and label.group.startswith("itememail"):
number_of_custom_email_labels += 1
group_name = "itememail%d" % (number_of_custom_email_labels+1)
email_obj.group = group_name
label_obj = self.vcard.add('x-ablabel')
label_obj.group = group_name
label_obj.value = custom_types[0]
def _get_post_addresses(self):
"""
: returns: dict of type and post address list
:rtype: dict(str, list(dict(str,list|str)))
"""
post_adr_dict = {}
for child in self.vcard.getChildren():
if child.name == "ADR":
type = helpers.list_to_string(
self._get_types_for_vcard_object(child, "home"), ", ")
if post_adr_dict.get(type) is None:
post_adr_dict[type] = []
post_adr_dict[type].append(
{
"box": child.value.box,
"extended": child.value.extended,
"street": child.value.street,
"code": child.value.code,
"city": child.value.city,
"region": child.value.region,
"country": child.value.country
})
# sort post address lists
for post_adr_list in post_adr_dict.values():
post_adr_list.sort(key=lambda x: (
helpers.list_to_string(x['city'], " ").lower(),
helpers.list_to_string(x['street'], " ").lower()))
return post_adr_dict
def _get_formatted_post_addresses(self):
formatted_post_adr_dict = {}
for type, post_adr_list in self._get_post_addresses().items():
formatted_post_adr_dict[type] = []
for post_adr in post_adr_list:
strings = []
if post_adr.get("street"):
strings.append(
helpers.list_to_string(post_adr.get("street"), "\n"))
if post_adr.get("box") and post_adr.get("extended"):
strings.append("%s %s" % (
helpers.list_to_string(post_adr.get("box"), " "),
helpers.list_to_string(post_adr.get("extended"), " ")))
elif post_adr.get("box"):
strings.append(
helpers.list_to_string(post_adr.get("box"), " "))
elif post_adr.get("extended"):
strings.append(
helpers.list_to_string(post_adr.get("extended"), " "))
if post_adr.get("code") and post_adr.get("city"):
strings.append("%s %s" % (
helpers.list_to_string(post_adr.get("code"), " "),
helpers.list_to_string(post_adr.get("city"), " ")))
elif post_adr.get("code"):
strings.append(
helpers.list_to_string(post_adr.get("code"), " "))
elif post_adr.get("city"):
strings.append(
helpers.list_to_string(post_adr.get("city"), " "))
if post_adr.get("region") and post_adr.get("country"):
strings.append("%s, %s" % (
helpers.list_to_string(post_adr.get("region"), " "),
helpers.list_to_string(post_adr.get("country"), " ")))
elif post_adr.get("region"):
strings.append(
helpers.list_to_string(post_adr.get("region"), " "))
elif post_adr.get("country"):
strings.append(
helpers.list_to_string(post_adr.get("country"), " "))
formatted_post_adr_dict[type].append('\n'.join(strings))
return formatted_post_adr_dict
def _add_post_address(self, type, box, extended, street, code, city,
region, country):
standard_types, custom_types, pref = self._parse_type_value(
helpers.string_to_list(type, ","), "%s, %s" % (street, city),
self.address_types_v4 if self.get_version() == "4.0" else
self.address_types_v3)
if not standard_types and not custom_types and pref == 0:
raise ValueError("Error: label for post address " + street +
" is missing.")
elif len(custom_types) > 1:
raise ValueError("Error: post address " + street + " got more "
"than one custom " "label: " +
helpers.list_to_string(custom_types, ", "))
else:
adr_obj = self.vcard.add('adr')
adr_obj.value = vobject.vcard.Address(
box=helpers.convert_to_vcard(
"box address field", box,
ObjectType.string_or_list_with_strings),
extended=helpers.convert_to_vcard(
"extended address field", extended,
ObjectType.string_or_list_with_strings),
street=helpers.convert_to_vcard(
"street", street, ObjectType.string_or_list_with_strings),
code=helpers.convert_to_vcard(
"post code", code, ObjectType.string_or_list_with_strings),
city=helpers.convert_to_vcard(
"city", city, ObjectType.string_or_list_with_strings),
region=helpers.convert_to_vcard(
"region", region, ObjectType.string_or_list_with_strings),
country=helpers.convert_to_vcard(
"country", country,
ObjectType.string_or_list_with_strings))
if self.get_version() == "4.0":
if pref > 0:
adr_obj.params['PREF'] = str(pref)
else:
if pref > 0:
standard_types.append("pref")
if standard_types:
adr_obj.params['TYPE'] = standard_types
if custom_types:
number_of_custom_post_address_labels = 0
for label in self.vcard.getChildren():
if label.name == "X-ABLABEL" \
and label.group.startswith("itemadr"):
number_of_custom_post_address_labels += 1
group_name = "itemadr%d" % (
number_of_custom_post_address_labels+1)
adr_obj.group = group_name
label_obj = self.vcard.add('x-ablabel')
label_obj.group = group_name
label_obj.value = custom_types[0]
def _get_categories(self):
"""
:rtype: list(str) or list(list(str))
"""
category_list = []
for child in self.vcard.getChildren():
if child.name == "CATEGORIES":
value = child.value
category_list.append(
value if isinstance(value, list) else [value])
if len(category_list) == 1:
return category_list[0]
return sorted(category_list)
def _add_category(self, categories):
""" categories variable must be a list """
categories_obj = self.vcard.add('categories')
categories_obj.value = helpers.convert_to_vcard(
"category", categories, ObjectType.list_with_strings)
def get_nicknames(self):
"""
:rtype: list(list(str))
"""
nicknames = []
for child in self.vcard.getChildren():
if child.name == "NICKNAME":
nicknames.append(child.value)
return sorted(nicknames)
def _add_nickname(self, nickname):
nickname_obj = self.vcard.add('nickname')
nickname_obj.value = helpers.convert_to_vcard(
"nickname", nickname, ObjectType.string)
def _get_notes(self):
"""
:rtype: list(list(str))
"""
notes = []
for child in self.vcard.getChildren():
if child.name == "NOTE":
notes.append(child.value)
return sorted(notes)
def _add_note(self, note):
note_obj = self.vcard.add('note')
note_obj.value = helpers.convert_to_vcard(
"note", note, ObjectType.string)
def _get_private_objects(self):
"""
:rtype: dict(str, list(str))
"""
private_objects = {}
for child in self.vcard.getChildren():
if child.name.lower().startswith("x-"):
try:
key_index = [
x.lower() for x in self.supported_private_objects
].index(child.name[2:].lower())
except ValueError:
pass
else:
key = self.supported_private_objects[key_index]
if private_objects.get(key) is None:
private_objects[key] = []
private_objects[key].append(child.value)
# sort private object lists
for value in private_objects.values():
value.sort()
return private_objects
def _add_private_object(self, key, value):
private_obj = self.vcard.add('X-' + key.upper())
private_obj.value = helpers.convert_to_vcard(
key, value, ObjectType.string)
def _get_webpages(self):
"""
:rtype: list(list(str))
"""
urls = []
for child in self.vcard.getChildren():
if child.name == "URL":
urls.append(child.value)
return sorted(urls)
def _add_webpage(self, webpage):
webpage_obj = self.vcard.add('url')
webpage_obj.value = helpers.convert_to_vcard(
"webpage", webpage, ObjectType.string)
def get_anniversary(self):
""":returns: contacts anniversary or None if not available
:rtype: datetime.datetime or str
"""
# vcard 4.0 could contain a single text value
try:
if self.vcard.anniversary.params.get("VALUE")[0] == "text":
return self.vcard.anniversary.value
except (AttributeError, IndexError, TypeError):
pass
# else try to convert to a datetime object
try:
return helpers.string_to_date(self.vcard.anniversary.value)
except (AttributeError, ValueError):
# vcard 3.0: x-anniversary (private object)
try:
return helpers.string_to_date(self.vcard.x_anniversary.value)
except (AttributeError, ValueError):
pass
return None
def get_formatted_anniversary(self):
return self._format_date_object(
self.get_anniversary(), self.localize_dates)
def _add_anniversary(self, date):
if isinstance(date, str):
if self.get_version() == "4.0":
anniversary_obj = self.vcard.add('anniversary')
anniversary_obj.params['VALUE'] = ["text"]
anniversary_obj.value = date.strip()
elif date.year == 1900 and date.month != 0 and date.day != 0 \
and date.hour == 0 and date.minute == 0 and date.second == 0 \
and self.get_version() == "4.0":
anniversary_obj = self.vcard.add('anniversary')
anniversary_obj.value = "--%.2d%.2d" % (date.month, date.day)
elif date.tzname() and date.tzname()[3:]:
if self.get_version() == "4.0":
anniversary_obj = self.vcard.add('anniversary')
anniversary_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2d%s" % (
date.year, date.month, date.day, date.hour, date.minute,
date.second, date.tzname()[3:])
else:
anniversary_obj = self.vcard.add('x-anniversary')
anniversary_obj.value = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2d%s" % (
date.year, date.month, date.day, date.hour, date.minute,
date.second, date.tzname()[3:])
elif date.hour != 0 or date.minute != 0 or date.second != 0:
if self.get_version() == "4.0":
anniversary_obj = self.vcard.add('anniversary')
anniversary_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2dZ" % (
date.year, date.month, date.day, date.hour, date.minute,
date.second)
else:
anniversary_obj = self.vcard.add('x-anniversary')
anniversary_obj.value = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ" % (
date.year, date.month, date.day, date.hour, date.minute,
date.second)
else:
if self.get_version() == "4.0":
anniversary_obj = self.vcard.add('anniversary')
anniversary_obj.value = "%.4d%.2d%.2d" % (date.year, date.month,
date.day)
else:
anniversary_obj = self.vcard.add('x-anniversary')
anniversary_obj.value = "%.4d-%.2d-%.2d" % (date.year,
date.month, date.day)
def get_birthday(self):
""":returns: contacts birthday or None if not available
:rtype: datetime.datetime or str
"""
# vcard 4.0 could contain a single text value
try:
if self.vcard.bday.params.get("VALUE")[0] == "text":
return self.vcard.bday.value
except (AttributeError, IndexError, TypeError):
pass
# else try to convert to a datetime object
try:
return helpers.string_to_date(self.vcard.bday.value)
except (AttributeError, ValueError):
pass
return None
def get_formatted_birthday(self):
return self._format_date_object(
self.get_birthday(), self.localize_dates)
def _add_birthday(self, date):
if isinstance(date, str):
if self.get_version() == "4.0":
bday_obj = self.vcard.add('bday')
bday_obj.params['VALUE'] = ["text"]
bday_obj.value = date.strip()
elif date.year == 1900 and date.month != 0 and date.day != 0 \
and date.hour == 0 and date.minute == 0 and date.second == 0 \
and self.get_version() == "4.0":
bday_obj = self.vcard.add('bday')
bday_obj.value = "--%.2d%.2d" % (date.month, date.day)
elif date.tzname() and date.tzname()[3:]:
bday_obj = self.vcard.add('bday')
if self.get_version() == "4.0":
bday_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2d%s" % (
date.year, date.month, date.day, date.hour, date.minute,
date.second, date.tzname()[3:])
else:
bday_obj.value = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2d%s" % (
date.year, date.month, date.day, date.hour, date.minute,
date.second, date.tzname()[3:])
elif date.hour != 0 or date.minute != 0 or date.second != 0:
bday_obj = self.vcard.add('bday')
if self.get_version() == "4.0":
bday_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2dZ" % (
date.year, date.month, date.day, date.hour, date.minute,
date.second)
else:
bday_obj.value = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ" % (
date.year, date.month, date.day, date.hour, date.minute,
date.second)
else:
bday_obj = self.vcard.add('bday')
if self.get_version() == "4.0":
bday_obj.value = "%.4d%.2d%.2d" % (date.year, date.month,
date.day)
else:
bday_obj.value = "%.4d-%.2d-%.2d" % (date.year, date.month,
date.day)
#######################
# object helper methods
#######################
@staticmethod
def _format_date_object(date, localize):
if date:
if isinstance(date, str):
return date
elif date.year == 1900 and date.month != 0 and date.day != 0 \
and date.hour == 0 and date.minute == 0 \
and date.second == 0:
return "--%.2d-%.2d" % (date.month, date.day)
elif (date.tzname() and date.tzname()[3:]) or \
(date.hour != 0 or date.minute != 0 or date.second != 0):
if localize:
return date.strftime(locale.nl_langinfo(locale.D_T_FMT))
else:
utc_offset=-time.timezone/60/60
return date.strftime(
"%Y-%m-%dT%H:%M:%S+" + str(int(utc_offset)).zfill(2) + ":00")
else:
if localize:
return date.strftime(locale.nl_langinfo(locale.D_FMT))
else:
return date.strftime("%Y-%m-%d")
return ""
@staticmethod
def _filter_invalid_tags(contents):
contents = re.sub('(?i)' + re.escape('X-messaging/aim-All'), 'X-AIM',
contents)
contents = re.sub('(?i)' + re.escape('X-messaging/gadu-All'),
'X-GADUGADU', contents)
contents = re.sub('(?i)' + re.escape('X-messaging/groupwise-All'),
'X-GROUPWISE', contents)
contents = re.sub('(?i)' + re.escape('X-messaging/icq-All'), 'X-ICQ',
contents)
contents = re.sub('(?i)' + re.escape('X-messaging/xmpp-All'),
'X-JABBER', contents)
contents = re.sub('(?i)' + re.escape('X-messaging/msn-All'), 'X-MSN',
contents)
contents = re.sub('(?i)' + re.escape('X-messaging/yahoo-All'),
'X-YAHOO', contents)
contents = re.sub('(?i)' + re.escape('X-messaging/skype-All'),
'X-SKYPE', contents)
contents = re.sub('(?i)' + re.escape('X-messaging/irc-All'), 'X-IRC',
contents)
contents = re.sub('(?i)' + re.escape('X-messaging/sip-All'), 'X-SIP',
contents)
return contents
def _process_user_input(self, input):
yaml_parser = YAML(typ='base')
# parse user input string
try:
contact_data = yaml_parser.load(input)
except ruamel.yaml.parser.ParserError as err:
raise ValueError(err)
except ruamel.yaml.scanner.ScannerError as err:
raise ValueError(err)
else:
if contact_data is None:
raise ValueError("Error: Found no contact information")
# check for available data
# at least enter name or organisation
if not bool(contact_data.get("First name")) \
and not bool(contact_data.get("Last name")) \
and not bool(contact_data.get("Organisation")):
raise ValueError(
"Error: You must either enter a name or an organisation")
# update rev
self.delete_vcard_object("REV")
self._add_rev(datetime.datetime.now())
# name
self.delete_vcard_object("FN")
self.delete_vcard_object("N")
# although the "n" attribute is not explisitely required by the vcard
# specification,
# the vobject library throws an exception, if it doesn't exist
# so add the name regardless if it's empty or not
self._add_name(
contact_data.get("Prefix") or "",
contact_data.get("First name") or "",
contact_data.get("Additional") or "",
contact_data.get("Last name") or "",
contact_data.get("Suffix") or "")
# nickname
self.delete_vcard_object("NICKNAME")
if bool(contact_data.get("Nickname")):
if isinstance(contact_data.get("Nickname"), str):
self._add_nickname(contact_data.get("Nickname"))
elif isinstance(contact_data.get("Nickname"), list):
for nickname in contact_data.get("Nickname"):
if bool(nickname):
self._add_nickname(nickname)
else:
raise ValueError(
"Error: nickname must be a string or a list of strings")
# organisation
self.delete_vcard_object("ORG")
self.delete_vcard_object("X-ABSHOWAS")
if bool(contact_data.get("Organisation")):
if isinstance(contact_data.get("Organisation"), str):
self._add_organisation([contact_data.get("Organisation")])
elif isinstance(contact_data.get("Organisation"), list):
for organisation in contact_data.get("Organisation"):
if bool(organisation):
if isinstance(organisation, str):
self._add_organisation([organisation])
else:
self._add_organisation(organisation)
else:
raise ValueError("Error: organisation must be a string or a "
"list of strings")
# role
self.delete_vcard_object("ROLE")
if bool(contact_data.get("Role")):
if isinstance(contact_data.get("Role"), str):
self._add_role(contact_data.get("Role"))
elif isinstance(contact_data.get("Role"), list):
for role in contact_data.get("Role"):
if bool(role):
self._add_role(role)
else:
raise ValueError(
"Error: role must be a string or a list of strings")
# title
self.delete_vcard_object("TITLE")
if bool(contact_data.get("Title")):
if isinstance(contact_data.get("Title"), str):
self._add_title(contact_data.get("Title"))
elif isinstance(contact_data.get("Title"), list):
for title in contact_data.get("Title"):
if bool(title):
self._add_title(title)
else:
raise ValueError(
"Error: title must be a string or a list of strings")
# phone
self.delete_vcard_object("TEL")
if contact_data.get("Phone"):
if isinstance(contact_data.get("Phone"), dict):
for type, number_list in contact_data.get("Phone").items():
if isinstance(number_list, str):
number_list = [number_list]
if isinstance(number_list, list):
for number in number_list:
if number:
self._add_phone_number(type, number)
else:
raise ValueError(
"Error: got no number or list of numbers for the "
"phone number type " + type)
else:
raise ValueError(
"Error: missing type value for phone number field")
# email
self.delete_vcard_object("EMAIL")
if contact_data.get("Email"):
if isinstance(contact_data.get("Email"), dict):
for type, email_list in contact_data.get("Email").items():
if isinstance(email_list, str):
email_list = [email_list]
if isinstance(email_list, list):
for email in email_list:
if email:
self.add_email_address(type, email)
else:
raise ValueError(
"Error: got no email or list of emails for the "
"email address type " + type)
else:
raise ValueError(
"Error: missing type value for email address field")
# post addresses
self.delete_vcard_object("ADR")
if contact_data.get("Address"):
if isinstance(contact_data.get("Address"), dict):
for type, post_adr_list in contact_data.get("Address").items():
if isinstance(post_adr_list, dict):
post_adr_list = [post_adr_list]
if isinstance(post_adr_list, list):
for post_adr in post_adr_list:
if isinstance(post_adr, dict):
address_not_empty = False
for key, value in post_adr.items():
if key in ["Box", "Extended", "Street",
"Code", "City", "Region",
"Country"] and bool(value):
address_not_empty = True
break
if address_not_empty:
self._add_post_address(
type, post_adr.get("Box") or "",
post_adr.get("Extended") or "",
post_adr.get("Street") or "",
post_adr.get("Code") or "",
post_adr.get("City") or "",
post_adr.get("Region") or "",
post_adr.get("Country") or "")
else:
raise ValueError(
"Error: one of the " + type + " type "
"address list items does not contain an "
"address")
else:
raise ValueError(
"Error: got no address or list of addresses for "
"the post address type " + type)
else:
raise ValueError(
"Error: missing type value for post address field")
# categories
self.delete_vcard_object("CATEGORIES")
if contact_data.get("Categories"):
if isinstance(contact_data.get("Categories"), str):
self._add_category([contact_data.get("Categories")])
elif isinstance(contact_data.get("Categories"), list):
only_contains_strings = True
for sub_category in contact_data.get("Categories"):
if not isinstance(sub_category, str):
only_contains_strings = False
break
# if the category list only contains strings, pack all of them
# in a single CATEGORIES vcard tag
if only_contains_strings:
self._add_category(contact_data.get("Categories"))
else:
for sub_category in contact_data.get("Categories"):
if sub_category:
if isinstance(sub_category, str):
self._add_category([sub_category])
else:
self._add_category(sub_category)
else:
raise ValueError(
"Error: category must be a string or a list of strings")
# urls
self.delete_vcard_object("URL")
if contact_data.get("Webpage"):
if isinstance(contact_data.get("Webpage"), str):
self._add_webpage(contact_data.get("Webpage"))
elif isinstance(contact_data.get("Webpage"), list):
for webpage in contact_data.get("Webpage"):
if webpage:
self._add_webpage(webpage)
else:
raise ValueError(
"Error: webpage must be a string or a list of strings")
# anniversary
self.delete_vcard_object("ANNIVERSARY")
self.delete_vcard_object("X-ANNIVERSARY")
if contact_data.get("Anniversary"):
if isinstance(contact_data.get("Anniversary"), str):
if re.match(r"^text[\s]*=.*$", contact_data.get("Anniversary")):
l = [x.strip() for x in
re.split("text[\s]*=", contact_data.get("Anniversary"))
if x.strip()]
if self.get_version() == "4.0":
date = ', '.join(l)
else:
raise ValueError(
"Error: Free text format for anniversary only "
"usable with vcard version 4.0.")
elif re.match(r"^--\d{4}$", contact_data.get("Anniversary")) \
and self.get_version() != "4.0":
raise ValueError(
"Error: Anniversary format --mmdd only usable with "
"vcard version 4.0. You may use 1900 as placeholder, "
"if the year of the anniversary is unknown.")
elif re.match(
r"^--\d{2}-\d{2}$", contact_data.get("Anniversary")) \
and self.get_version() != "4.0":
raise ValueError(
"Error: Anniversary format --mm-dd only usable with "
"vcard version 4.0. You may use 1900 as placeholder, "
"if the year of the anniversary is unknown.")
else:
try:
date = helpers.string_to_date(
contact_data.get("Anniversary"))
except ValueError:
raise ValueError(
"Error: Wrong anniversary format or invalid date\n"
"Use format yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS")
if date:
self._add_anniversary(date)
else:
raise ValueError("Error: anniversary must be a string object.")
# birthday
self.delete_vcard_object("BDAY")
if contact_data.get("Birthday"):
if isinstance(contact_data.get("Birthday"), str):
if re.match(r"^text[\s]*=.*$", contact_data.get("Birthday")):
l = [x.strip() for x in
re.split("text[\s]*=", contact_data.get("Birthday"))
if x.strip()]
if self.get_version() == "4.0":
date = ', '.join(l)
else:
raise ValueError(
"Error: Free text format for birthday only usable "
"with vcard version 4.0.")
elif re.match(r"^--\d{4}$", contact_data.get("Birthday")) \
and self.get_version() != "4.0":
raise ValueError(
"Error: Birthday format --mmdd only usable with "
"vcard version 4.0. You may use 1900 as placeholder, "
"if the year of birth is unknown.")
elif re.match(
r"^--\d{2}-\d{2}$", contact_data.get("Birthday")) \
and self.get_version() != "4.0":
raise ValueError(
"Error: Birthday format --mm-dd only usable with "
"vcard version 4.0. You may use 1900 as placeholder, "
"if the year of birth is unknown.")
else:
try:
date = helpers.string_to_date(
contact_data.get("Birthday"))
except ValueError:
raise ValueError(
"Error: Wrong birthday format or invalid date\n"
"Use format yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS")
if date:
self._add_birthday(date)
else:
raise ValueError("Error: birthday must be a string object.")
# private objects
for supported in self.supported_private_objects:
self.delete_vcard_object("X-%s" % supported.upper())
if contact_data.get("Private"):
if isinstance(contact_data.get("Private"), dict):
for key, value_list in contact_data.get("Private").items():
if key in self.supported_private_objects:
if isinstance(value_list, str):
value_list = [value_list]
if isinstance(value_list, list):
for value in value_list:
if value:
self._add_private_object(key, value)
else:
raise ValueError(
"Error: got no value or list of values for "
"the private object " + key)
else:
raise ValueError(
"Error: private object key " + key + " was "
"changed.\nSupported private keys: " + ', '.join(
self.supported_private_objects))
else:
raise ValueError("Error: private objects must consist of a "
"key : value pair.")
# notes
self.delete_vcard_object("NOTE")
if contact_data.get("Note"):
if isinstance(contact_data.get("Note"), str):
self._add_note(contact_data.get("Note"))
elif isinstance(contact_data.get("Note"), list):
for note in contact_data.get("Note"):
if note:
self._add_note(note)
else:
raise ValueError(
"Error: note must be a string or a list of strings\n"
"Use the | character to create a multi-line note.")
def get_template(self):
strings = []
for line in helpers.get_new_contact_template().splitlines():
if line.startswith("#"):
strings.append(line)
elif line == "":
strings.append(line)
elif line.lower().startswith("prefix"):
strings += helpers.convert_to_yaml(
"Prefix", self._get_name_prefixes(), 0, 11, True)
elif line.lower().startswith("first name"):
strings += helpers.convert_to_yaml(
"First name", self._get_first_names(), 0, 11, True)
elif line.lower().startswith("additional"):
strings += helpers.convert_to_yaml(
"Additional", self._get_additional_names(), 0, 11, True)
elif line.lower().startswith("last name"):
strings += helpers.convert_to_yaml(
"Last name", self._get_last_names(), 0, 11, True)
elif line.lower().startswith("suffix"):
strings += helpers.convert_to_yaml(
"Suffix", self._get_name_suffixes(), 0, 11, True)
elif line.lower().startswith("nickname"):
strings += helpers.convert_to_yaml(
"Nickname", self.get_nicknames(), 0, 9, True)
elif line.lower().startswith("organisation"):
strings += helpers.convert_to_yaml(
"Organisation", self._get_organisations(), 0, 13, True)
elif line.lower().startswith("title"):
strings += helpers.convert_to_yaml(
"Title", self._get_titles(), 0, 6, True)
elif line.lower().startswith("role"):
strings += helpers.convert_to_yaml(
"Role", self._get_roles(), 0, 6, True)
elif line.lower().startswith("phone"):
strings.append("Phone :")
if not self.get_phone_numbers().keys():
strings.append(" cell : ")
strings.append(" home : ")
else:
longest_key = max(self.get_phone_numbers().keys(), key=len)
for type, number_list in sorted(
self.get_phone_numbers().items(),
key=lambda k: k[0].lower()):
strings += helpers.convert_to_yaml(
type, number_list, 4, len(longest_key)+1, True)
elif line.lower().startswith("email"):
strings.append("Email :")
if not self.get_email_addresses().keys():
strings.append(" home : ")
strings.append(" work : ")
else:
longest_key = max(self.get_email_addresses().keys(),
key=len)
for type, email_list in sorted(
self.get_email_addresses().items(),
key=lambda k: k[0].lower()):
strings += helpers.convert_to_yaml(
type, email_list, 4, len(longest_key)+1, True)
elif line.lower().startswith("address"):
strings.append("Address :")
if not self._get_post_addresses().keys():
strings.append(" home :")
strings.append(" Box : ")
strings.append(" Extended : ")
strings.append(" Street : ")
strings.append(" Code : ")
strings.append(" City : ")
strings.append(" Region : ")
strings.append(" Country : ")
else:
for type, post_adr_list in sorted(
self._get_post_addresses().items(),
key=lambda k: k[0].lower()):
strings.append(" %s:" % type)
for post_adr in post_adr_list:
indentation = 8
if len(post_adr_list) > 1:
indentation += 4
strings.append(" -")
strings += helpers.convert_to_yaml(
"Box", post_adr.get("box"), indentation, 9,
True)
strings += helpers.convert_to_yaml(
"Extended", post_adr.get("extended"),
indentation, 9, True)
strings += helpers.convert_to_yaml(
"Street", post_adr.get("street"), indentation,
9, True)
strings += helpers.convert_to_yaml(
"Code", post_adr.get("code"), indentation, 9,
True)
strings += helpers.convert_to_yaml(
"City", post_adr.get("city"), indentation, 9,
True)
strings += helpers.convert_to_yaml(
"Region", post_adr.get("region"), indentation,
9, True)
strings += helpers.convert_to_yaml(
"Country", post_adr.get("country"),
indentation, 9, True)
elif line.lower().startswith("private"):
strings.append("Private :")
if self.supported_private_objects:
longest_key = max(self.supported_private_objects, key=len)
for object in self.supported_private_objects:
strings += helpers.convert_to_yaml(
object, self._get_private_objects().get(object)
or "", 4, len(longest_key)+1, True)
elif line.lower().startswith("anniversary"):
anniversary = self.get_anniversary()
if anniversary:
if isinstance(anniversary, str):
strings.append("Anniversary : text= %s" % anniversary)
elif anniversary.year == 1900 and anniversary.month != 0 and \
anniversary.day != 0 and anniversary.hour == 0 and \
anniversary.minute == 0 and anniversary.second == 0 and \
self.get_version() == "4.0":
strings.append("Anniversary : --%.2d-%.2d"
% (anniversary.month, anniversary.day))
elif (anniversary.tzname() and anniversary.tzname()[3:]) or \
(anniversary.hour != 0 or anniversary.minute != 0
or anniversary.second != 0):
strings.append("Anniversary : %s" % anniversary.isoformat())
else:
strings.append("Anniversary : %.4d-%.2d-%.2d" % (
anniversary.year, anniversary.month, anniversary.day))
else:
strings.append("Anniversary : ")
elif line.lower().startswith("birthday"):
birthday = self.get_birthday()
if birthday:
if isinstance(birthday, str):
strings.append("Birthday : text= %s" % birthday)
elif birthday.year == 1900 and birthday.month != 0 and \
birthday.day != 0 and birthday.hour == 0 and \
birthday.minute == 0 and birthday.second == 0 and \
self.get_version() == "4.0":
strings.append("Birthday : --%.2d-%.2d"
% (birthday.month, birthday.day))
elif (birthday.tzname() and birthday.tzname()[3:]) or \
(birthday.hour != 0 or birthday.minute != 0
or birthday.second != 0):
strings.append("Birthday : %s" % birthday.isoformat())
else:
strings.append("Birthday : %.4d-%.2d-%.2d" % (
birthday.year, birthday.month, birthday.day))
else:
strings.append("Birthday : ")
elif line.lower().startswith("categories"):
strings += helpers.convert_to_yaml(
"Categories", self._get_categories(), 0, 11, True)
elif line.lower().startswith("note"):
strings += helpers.convert_to_yaml(
"Note", self._get_notes(), 0, 5, True)
elif line.lower().startswith("webpage"):
strings += helpers.convert_to_yaml(
"Webpage", self._get_webpages(), 0, 8, True)
return '\n'.join(strings) + "\n" # posix standard: eof char must be \n
def print_vcard(self, show_address_book=True, show_uid=True):
strings = []
# name
if self._get_first_names() or self._get_last_names():
names = []
if self._get_name_prefixes():
names += self._get_name_prefixes()
if self._get_first_names():
names += self._get_first_names()
if self._get_additional_names():
names += self._get_additional_names()
if self._get_last_names():
names += self._get_last_names()
if self._get_name_suffixes():
names += self._get_name_suffixes()
strings.append("Name: %s" % helpers.list_to_string(names, " "))
# organisation
if self._get_organisations():
strings += helpers.convert_to_yaml(
"Organisation", self._get_organisations(), 0, -1, False)
# address book name
if show_address_book:
strings.append("Address book: %s" % self.address_book.name)
# person related information
if self.get_birthday() is not None or self.get_anniversary() is not None \
or self.get_nicknames() or self._get_roles() or self._get_titles():
strings.append("General:")
if self.get_anniversary():
strings.append(" Anniversary: %s"
% self.get_formatted_anniversary())
if self.get_birthday():
strings.append(" Birthday: %s"
% self.get_formatted_birthday())
if self.get_nicknames():
strings += helpers.convert_to_yaml(
"Nickname", self.get_nicknames(), 4, -1, False)
if self._get_roles():
strings += helpers.convert_to_yaml(
"Role", self._get_roles(), 4, -1, False)
if self._get_titles():
strings += helpers.convert_to_yaml(
"Title", self._get_titles(), 4, -1, False)
# phone numbers
if self.get_phone_numbers().keys():
strings.append("Phone")
for type, number_list in sorted(
self.get_phone_numbers().items(),
key=lambda k: k[0].lower()):
strings += helpers.convert_to_yaml(
type, number_list, 4, -1, False)
# email addresses
if self.get_email_addresses().keys():
strings.append("E-Mail")
for type, email_list in sorted(
self.get_email_addresses().items(),
key=lambda k: k[0].lower()):
strings += helpers.convert_to_yaml(
type, email_list, 4, -1, False)
# post addresses
if self._get_post_addresses().keys():
strings.append("Address")
for type, post_adr_list in sorted(
self._get_formatted_post_addresses().items(),
key=lambda k: k[0].lower()):
strings += helpers.convert_to_yaml(
type, post_adr_list, 4, -1, False)
# private objects
if self._get_private_objects().keys():
strings.append("Private:")
for object in self.supported_private_objects:
if self._get_private_objects().get(object):
strings += helpers.convert_to_yaml(
object, self._get_private_objects().get(object), 4, -1,
False)
# misc stuff
if self._get_categories() or (show_uid and self.get_uid() != "") \
or self._get_webpages() or self._get_notes():
strings.append("Miscellaneous")
if show_uid and self.get_uid():
strings.append(" UID: %s" % self.get_uid())
if self._get_categories():
strings += helpers.convert_to_yaml(
"Categories", self._get_categories(), 4, -1, False)
if self._get_webpages():
strings += helpers.convert_to_yaml(
"Webpage", self._get_webpages(), 4, -1, False)
if self._get_notes():
strings += helpers.convert_to_yaml(
"Note", self._get_notes(), 4, -1, False)
return '\n'.join(strings)
def write_to_file(self, overwrite=False):
# make sure, that every contact contains a uid
if not self.get_uid():
self.add_uid(helpers.get_random_uid())
try:
with atomic_write(self.filename, overwrite=overwrite) as f:
f.write(self.vcard.serialize())
except vobject.base.ValidateError as err:
print("Error: Vcard is not valid.\n%s" % err)
sys.exit(4)
except IOError as err:
print("Error: Can't write\n%s" % err)
sys.exit(4)
except OSError as err:
print("Error: vcard with the file name %s already exists\n%s"
% (os.path.basename(self.filename), err))
sys.exit(4)
def delete_vcard_object(self, object_name):
# first collect all vcard items, which should be removed
to_be_removed = []
for child in self.vcard.getChildren():
if child.name == object_name:
if child.group:
for label in self.vcard.getChildren():
if label.name == "X-ABLABEL" and \
label.group == child.group:
to_be_removed.append(label)
to_be_removed.append(child)
# then delete them one by one
for item in to_be_removed:
self.vcard.remove(item)
def delete_vcard_file(self):
if os.path.exists(self.filename):
os.remove(self.filename)
else:
print("Error: Vcard file %s does not exist." % self.filename)
sys.exit(4)
#######################
# static helper methods
#######################
def _get_types_for_vcard_object(self, object, default_type):
"""
get list of types for phone number, email or post address
:param object: vcard class object
:type object: vobject.vCard
:param default_type: use if the object contains no type
:type default_type: str
:returns: list of type labels
:rtype: list(str)
"""
type_list = []
# try to find label group for custom value type
if object.group:
for label in self.vcard.getChildren():
if label.name == "X-ABLABEL" and label.group == object.group:
custom_type = label.value.strip()
if custom_type:
type_list.append(custom_type)
# then load type from params dict
standard_types = object.params.get("TYPE")
if standard_types is not None:
if not isinstance(standard_types, list):
standard_types = [standard_types]
for type in standard_types:
type = type.strip()
if type and type.lower() != "pref":
if not type.lower().startswith("x-"):
type_list.append(type)
elif type[2:].lower() not in \
[x.lower() for x in type_list]:
# add x-custom type in case it's not already added by
# custom label for loop above but strip x- before
type_list.append(type[2:])
# try to get pref parameter from vcard version 4.0
try:
type_list.append("pref=%d" % int(object.params.get("PREF")[0]))
except (IndexError, TypeError, ValueError):
# else try to determine, if type params contain pref attribute
try:
for x in object.params.get("TYPE"):
if x.lower() == "pref" and "pref" not in type_list:
type_list.append("pref")
except TypeError:
pass
# return type_list or default type
if type_list:
return type_list
return [default_type]
@staticmethod
def _parse_type_value(types, value, supported_types):
"""Parse type value of phone numbers, email and post addresses.
:param types: list of type values
:type types: list(str)
:param value: the corresponding label, required for more verbose
exceptions
:type value: str
:param supported_types: all allowed standard types
:type supported_types: list(str)
:returns: tuple of standard and custom types and pref integer
:rtype: tuple(list(str), list(str), int)
"""
custom_types = []
standard_types = []
pref = 0
for type in types:
type = type.strip()
if type:
if type.lower() in supported_types:
standard_types.append(type)
elif type.lower() == "pref":
pref += 1
elif re.match(r"^pref=\d{1,2}$", type.lower()):
pref += int(type.split("=")[1])
else:
if type.lower().startswith("x-"):
custom_types.append(type[2:])
standard_types.append(type)
else:
custom_types.append(type)
standard_types.append("X-%s" % type)
return (standard_types, custom_types, pref)
khard-0.12.2/khard/config.py 0000664 0000000 0000000 00000037660 13230747056 0015620 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# singleton code comes from:
# http://code.activestate.com/recipes/52558/#as_content
from distutils.spawn import find_executable
import locale
import logging
import os
import re
import sys
import configobj
from .actions import Actions
from .address_book import AddressBook
from . import helpers
def exit(message, prefix="Error in config file\n"):
"""Exit with a message and a return code indicating an error in the config
file.
This function doesn't return, it calls sys.exit.
:param message: the message to print
:type message: str
:param prefix: the prefix to put in front of the message
:type prefix: str
:returns: does not return
"""
print(prefix+message)
sys.exit(3)
class Config:
supported_vcard_versions = ("3.0", "4.0")
def __init__(self, config_file=""):
self.config = None
self.address_book_list = []
self.original_uid_dict = {}
self.uid_dict = {}
# set locale
locale.setlocale(locale.LC_ALL, '')
# load config file
if config_file == "":
xdg_config_home = os.getenv("XDG_CONFIG_HOME",
os.path.expanduser("~/.config"))
config_file = os.getenv("KHARD_CONFIG", os.path.join(
xdg_config_home, "khard", "khard.conf"))
if not os.path.exists(config_file):
exit("Config file %s not available" % config_file, prefix="")
# parse config file contents
try:
self.config = configobj.ConfigObj(config_file, interpolation=False)
except configobj.ParseError as err:
exit(str(err))
# general settings
if "general" not in self.config:
self.config['general'] = {}
# debug
self._convert_boolean_config_value(self.config["general"],
"debug", False)
self.debug = self.config["general"]["debug"]
# editor
self.editor = self.config["general"].get("editor") \
or os.environ.get("EDITOR")
if self.editor is None:
exit("Set path to your preferred text editor in khard's config "
"file or the $EDITOR shell variable\n"
"Example for khard.conf: editor = vim")
self.editor = find_executable(os.path.expanduser(self.editor))
if self.editor is None:
exit("Invalid editor path or executable not found.")
# merge editor
self.merge_editor = self.config['general'].get("merge_editor") \
or os.environ.get("MERGE_EDITOR")
if self.merge_editor is None:
exit("Set path to your preferred text merge editor in khard's "
"config file or the $MERGE_EDITOR shell variable\n"
"Example for khard.conf: merge_editor = vimdiff")
self.merge_editor = find_executable(os.path.expanduser(
self.merge_editor))
if self.merge_editor is None:
exit("Invalid merge editor path or executable not found.")
# default action
self.default_action = self.config["general"].get("default_action", "list")
if self.default_action is None:
exit("Missing default action parameter.")
elif self.default_action not in Actions.get_list_of_all_actions():
exit("Invalid value for default_action parameter\n"
"Possible values: %s" % ', '.join(
sorted(Actions.get_list_of_all_actions())))
# contact table settings
if "contact table" not in self.config:
self.config['contact table'] = {}
# sort contact table by first or last name
self.sort = self.config["contact table"].get("sort", "first_name")
if self.sort not in ["first_name", "last_name"]:
exit("Invalid value for sort parameter\n"
"Possible values: first_name, last_name")
# display names in contact table by first or last name
if "display" not in self.config['contact table']:
# if display by name attribute is not present in the config file
# use the sort attribute value for backwards compatibility
self.config['contact table']['display'] = self.sort
elif self.config['contact table']['display'] not in ["first_name",
"last_name"]:
exit("Invalid value for display parameter\n"
"Possible values: first_name, last_name")
# reverse contact table
self._convert_boolean_config_value(self.config["contact table"],
"reverse", False)
# group contact table by address book
self._convert_boolean_config_value(self.config["contact table"],
"group_by_addressbook", False)
# nickname
self._convert_boolean_config_value(self.config["contact table"],
"show_nicknames", False)
# show uids
self._convert_boolean_config_value(self.config["contact table"],
"show_uids", True)
# localize dates
self._convert_boolean_config_value(self.config["contact table"],
"localize_dates", True)
# vcard settings
if "vcard" not in self.config:
self.config['vcard'] = {}
# get supported private objects
if "private_objects" not in self.config['vcard'] \
or not self.config['vcard']['private_objects']:
self.config['vcard']['private_objects'] = []
else:
if not isinstance(self.config['vcard']['private_objects'], list):
self.config['vcard']['private_objects'] = [
self.config['vcard']['private_objects']]
# check if object only contains letters, digits or -
for object in self.config['vcard']['private_objects']:
if object != re.sub("[^a-zA-Z0-9-]", "", object):
exit("private object %s may only contain letters, digits "
"and the \"-\" character." % object)
if object == re.sub("[^-]", "", object) \
or object.startswith("-") or object.endswith("-"):
exit("A \"-\" in a private object label must be at least "
"surrounded by one letter or digit.")
# preferred vcard version
if "preferred_version" not in self.config['vcard']:
self.config['vcard']['preferred_version'] = "3.0"
elif self.config['vcard']['preferred_version'] not in \
self.supported_vcard_versions:
exit("Invalid value for preferred_version parameter\n"
"Possible values: %s" % self.supported_vcard_versions)
# speed up program by pre-searching in the vcard source files
self._convert_boolean_config_value(self.config["vcard"],
"search_in_source_files", False)
# skip unparsable vcards
self._convert_boolean_config_value(self.config["vcard"],
"skip_unparsable", False)
# load address books
if "addressbooks" not in self.config:
exit('Missing main section "[addressbooks]".')
if not self.config['addressbooks'].keys():
exit("No address book entries available.")
for name in self.config['addressbooks'].keys():
# create address book object
try:
address_book = AddressBook(
name, self.config['addressbooks'][name]['path'])
except KeyError:
exit("Missing path to the \"%s\" address book." % name)
except IOError as err:
exit(str(err))
else:
# add address book to list
self.address_book_list.append(address_book)
@staticmethod
def _convert_boolean_config_value(config, name, default=True):
"""Convert the named field to a bool represented by its previous string
value. If no such field was present use the default.
:param config: the config section where to set the option
:type config: configobj.ConfigObj
:param name: the name of the option to convert
:type name: str
:param default: the default value to use if the option was not
previously set
:type default: bool
:returns: None
"""
if name not in config:
config[name] = default
elif config[name] == "yes":
config[name] = True
elif config[name] == "no":
config[name] = False
else:
raise ValueError("Error in config file\nInvalid value for %s "
"parameter\nPossible values: yes, no" % name)
def get_all_address_books(self):
"""
return a list of all address books from config file
But due to performance optimizations its not guaranteed, that the
address books already contain their contact objects
if you must be sure, get every address book individually with the
get_address_book() function below
:rtype: list(AddressBook)
"""
return self.address_book_list
def get_address_book(self, name, search_queries=None):
"""
return address book object or None, if the address book with the
given name does not exist
:rtype: AddressBook
"""
if not self.search_in_source_files():
search_queries = None
for address_book in self.address_book_list:
if name == address_book.name:
if not address_book.loaded:
# load vcard files of address book
contacts, errors = address_book.load_all_vcards(
self.get_supported_private_objects(),
self.localize_dates(), search_queries)
# check if one or more contacts could not be parsed
if errors > 0:
if not self.skip_unparsable():
logging.error(
"%d of %d vcard files of address book %s "
"could not be parsed\nUse --debug for more "
"information or --skip-unparsable to proceed",
errors, contacts, name)
sys.exit(2)
else:
logging.debug(
"\n%d of %d vcard files of address book %s "
"could not be parsed\n", errors, contacts, name)
# Check uniqueness of vcard uids and create short uid
# dictionary that can be disabled with the show_uids option
# in the config file, if desired.
if self.config['contact table']['show_uids']:
# check, if multiple contacts have the same uid
for contact in address_book.contact_list:
uid = contact.get_uid()
if uid:
matching_contact = self.original_uid_dict.get(
uid)
if matching_contact is None:
self.original_uid_dict[uid] = contact
else:
exit("The contact %s from address book %s "
"and the contact %s from address book "
"%s have the same uid %s" % (
matching_contact.get_full_name(),
matching_contact.address_book.name,
contact.get_full_name(),
contact.address_book.name,
contact.get_uid()), prefix="")
# rebuild shortened uid dictionary
self._create_shortened_uid_dictionary()
return address_book
# Return None if no address book did match the given name.
return None
def has_uids(self):
return len(self.uid_dict.keys()) > 0
def _create_shortened_uid_dictionary(self):
# uniqueness of uids is guaranteed but they are much to long for the -u
# / --uid command line option
#
# Therefore clear previously filled uid_dict and recreate with the
# shortest possible uids, so they are still unique but much handier
#
# with around 100 contacts that short id should not be longer then two
# or three characters
self.uid_dict.clear()
flat_contact_list = sorted(self.original_uid_dict.values(),
key=lambda x: x.get_uid())
if len(flat_contact_list) == 1:
current = flat_contact_list[0]
self.uid_dict[current.get_uid()[:1]] = current
elif len(flat_contact_list) > 1:
# first list element
current = flat_contact_list[0]
next = flat_contact_list[1]
same = helpers.compare_uids(current.get_uid(), next.get_uid())
self.uid_dict[current.get_uid()[:same+1]] = current
# list elements 1 to len(flat_contact_list)-1
for index in range(1, len(flat_contact_list)-1):
prev = flat_contact_list[index-1]
current = flat_contact_list[index]
next = flat_contact_list[index+1]
same = max(helpers.compare_uids(prev.get_uid(),
current.get_uid()),
helpers.compare_uids(current.get_uid(),
next.get_uid()))
self.uid_dict[current.get_uid()[:same+1]] = current
# last list element
prev = flat_contact_list[-2]
current = flat_contact_list[-1]
same = helpers.compare_uids(prev.get_uid(), current.get_uid())
self.uid_dict[current.get_uid()[:same+1]] = current
def get_shortened_uid(self, uid):
if uid:
for length_of_uid in range(len(uid), 0, -1):
if self.uid_dict.get(uid[:length_of_uid]) is not None:
return uid[:length_of_uid]
return ""
def localize_dates(self):
return self.config['contact table']['localize_dates']
def get_supported_private_objects(self):
return self.config['vcard']['private_objects']
def get_preferred_vcard_version(self):
return self.config['vcard']['preferred_version']
def set_preferred_vcard_version(self, vcard_version):
self.config['vcard']['preferred_version'] = vcard_version
def search_in_source_files(self):
return self.config['vcard']['search_in_source_files']
def set_search_in_source_files(self, bool):
self.config['vcard']['search_in_source_files'] = bool
def skip_unparsable(self):
return self.config['vcard']['skip_unparsable']
def set_skip_unparsable(self, bool):
self.config['vcard']['skip_unparsable'] = bool
def display_by_name(self):
return self.config['contact table']['display']
def set_display_by_name(self, criteria):
self.config['contact table']['display'] = criteria
def group_by_addressbook(self):
return self.config['contact table']['group_by_addressbook']
def set_group_by_addressbook(self, bool):
self.config['contact table']['group_by_addressbook'] = bool
def reverse(self):
return self.config['contact table']['reverse']
def set_reverse(self, bool):
self.config['contact table']['reverse'] = bool
def show_nicknames(self):
return self.config['contact table']['show_nicknames']
khard-0.12.2/khard/helpers.py 0000664 0000000 0000000 00000031540 13230747056 0016004 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import os
import random
import string
from datetime import datetime
from textwrap import dedent
from .object_type import ObjectType
def pretty_print(table, justify="L"):
# get width for every column
column_widths = [0] * table[0].__len__()
offset = 3
for row in table:
for index, col in enumerate(row):
width = len(str(col))
if width > column_widths[index]:
column_widths[index] = width
table_row_list = []
for row in table:
single_row_list = []
for col_index, col in enumerate(row):
if justify == "R": # justify right
formated_column = str(col).rjust(column_widths[col_index] +
offset)
elif justify == "L": # justify left
formated_column = str(col).ljust(column_widths[col_index] +
offset)
elif justify == "C": # justify center
formated_column = str(col).center(column_widths[col_index] +
offset)
single_row_list.append(formated_column)
table_row_list.append(' '.join(single_row_list))
return '\n'.join(table_row_list)
def list_to_string(input, delimiter):
"""
converts list to string recursively so that nested lists are supported
"""
if isinstance(input, list):
flat_list = []
for item in input:
flat_list.append(list_to_string(item, delimiter))
return delimiter.join(flat_list)
return input
def string_to_list(input, delimiter):
if isinstance(input, list):
return input
return [x.strip() for x in input.split(delimiter)]
def string_to_date(input):
"""convert string to date object"""
# try date format --mmdd
try:
return datetime.strptime(input, "--%m%d")
except ValueError:
pass
# try date format --mm-dd
try:
return datetime.strptime(input, "--%m-%d")
except ValueError:
pass
# try date format yyyymmdd
try:
return datetime.strptime(input, "%Y%m%d")
except ValueError:
pass
# try date format yyyy-mm-dd
try:
return datetime.strptime(input, "%Y-%m-%d")
except ValueError:
pass
# try datetime format yyyymmddThhmmss
try:
return datetime.strptime(input, "%Y%m%dT%H%M%S")
except ValueError:
pass
# try datetime format yyyy-mm-ddThh:mm:ss
try:
return datetime.strptime(input, "%Y-%m-%dT%H:%M:%S")
except ValueError:
pass
# try datetime format yyyymmddThhmmssZ
try:
return datetime.strptime(input, "%Y%m%dT%H%M%SZ")
except ValueError:
pass
# try datetime format yyyy-mm-ddThh:mm:ssZ
try:
return datetime.strptime(input, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
pass
# try datetime format yyyymmddThhmmsstz where tz may look like -06:00
try:
return datetime.strptime(''.join(input.rsplit(":", 1)),
"%Y%m%dT%H%M%S%z")
except ValueError:
pass
# try datetime format yyyy-mm-ddThh:mm:sstz where tz may look like -06:00
try:
return datetime.strptime(''.join(input.rsplit(":", 1)),
"%Y-%m-%dT%H:%M:%S%z")
except ValueError:
pass
raise ValueError
def get_random_uid():
return ''.join([random.choice(string.ascii_lowercase + string.digits)
for _ in range(36)])
def compare_uids(uid1, uid2):
sum = 0
for c1, c2 in zip(uid1, uid2):
if c1 == c2:
sum += 1
else:
break
return sum
def file_modification_date(filename):
t = os.path.getmtime(filename)
return datetime.fromtimestamp(t)
def convert_to_yaml(
name, value, indentation, indexOfColon, show_multi_line_character):
"""converts a value list into yaml syntax
:param name: name of object (example: phone)
:type name: str
:param value: object contents
:type value: str, list(str), list(list(str))
:param indentation: indent all by number of spaces
:type indentation: int
:param indexOfColon: use to position : at the name string (-1 for no space)
:type indexOfColon: int
:param show_multi_line_character: option to hide "|"
:type show_multi_line_character: boolean
:returns: yaml formatted string array of name, value pair
:rtype: list(str)
"""
strings = []
if isinstance(value, list):
# special case for single item lists:
if len(value) == 1 \
and isinstance(value[0], str):
# value = ["string"] should not be converted to
# name:
# - string
# but to "name: string" instead
value = value[0]
elif len(value) == 1 \
and isinstance(value[0], list) \
and len(value[0]) == 1 \
and isinstance(value[0][0], str):
# same applies to value = [["string"]]
value = value[0][0]
if isinstance(value, str):
strings.append("%s%s%s: %s" % (
' ' * indentation, name, ' ' * (indexOfColon-len(name)),
indent_multiline_string(value, indentation+4,
show_multi_line_character)))
elif isinstance(value, list):
strings.append("%s%s%s: " % (
' ' * indentation, name, ' ' * (indexOfColon-len(name))))
for outer in value:
# special case for single item sublists
if isinstance(outer, list) \
and len(outer) == 1 \
and isinstance(outer[0], str):
# outer = ["string"] should not be converted to
# -
# - string
# but to "- string" instead
outer = outer[0]
if isinstance(outer, str):
strings.append("%s- %s" % (
' ' * (indentation+4), indent_multiline_string(
outer, indentation+8, show_multi_line_character)))
elif isinstance(outer, list):
strings.append("%s- " % (' ' * (indentation+4)))
for inner in outer:
if isinstance(inner, str):
strings.append("%s- %s" % (
' ' * (indentation+8), indent_multiline_string(
inner, indentation+12,
show_multi_line_character)))
return strings
def convert_to_vcard(name, value, allowed_object_type):
"""converts user input into vcard compatible data structures
:param name: object name, only required for error messages
:type name: str
:param value: user input
:type value: str or list(str)
:param allowed_object_type: set the accepted return type for vcard
attribute
:type allowed_object_type: enum of type ObjectType
:returns: cleaned user input, ready for vcard or a ValueError
:rtype: str or list(str)
"""
if isinstance(value, str):
if allowed_object_type == ObjectType.list_with_strings:
raise ValueError(
"Error: " + name + " must not contain a single string.")
else:
return value.strip()
elif isinstance(value, list):
if allowed_object_type == ObjectType.string:
raise ValueError(
"Error: " + name + " must not contain a list.")
else:
for entry in value:
if not isinstance(entry, str):
raise ValueError(
"Error: " + name + " must not contain a nested list")
# filter out empty list items and strip leading and trailing space
return [x.strip() for x in value if x]
else:
if allowed_object_type == ObjectType.string:
raise ValueError(
"Error: " + name + " must be a string.")
elif allowed_object_type == ObjectType.list_with_strings:
raise ValueError(
"Error: " + name + " must be a list with strings.")
else:
raise ValueError(
"Error: " + name + " must be a string or a list with strings.")
def indent_multiline_string(input, indentation, show_multi_line_character):
# if input is a list, convert to string first
if isinstance(input, list):
input = list_to_string(input, "")
# format multiline string
if "\n" in input:
lines = ["|"] if show_multi_line_character else [""]
for line in input.split("\n"):
lines.append("%s%s" % (' ' * indentation, line.strip()))
return '\n'.join(lines)
return input.strip()
def get_new_contact_template(supported_private_objects=[]):
formatted_private_objects = []
if supported_private_objects:
formatted_private_objects.append("")
longest_key = max(supported_private_objects, key=len)
for object in supported_private_objects:
formatted_private_objects += convert_to_yaml(
object, "", 12, len(longest_key)+1, True)
# create template
return dedent("""
# name components
# every entry may contain a string or a list of strings
# format:
# First name : name1
# Additional :
# - name2
# - name3
# Last name : name4
Prefix :
First name :
Additional :
Last name :
Suffix :
# nickname
# may contain a string or a list of strings
Nickname :
# important dates
# Formats:
# vcard 3.0 and 4.0: yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS
# vcard 4.0 only: --mm-dd or text= string value
# anniversary
Anniversary :
# birthday
Birthday :
# organisation
# format:
# Organisation : company
# or
# Organisation :
# - company1
# - company2
# or
# Organisation :
# -
# - company
# - unit
Organisation :
# organisation title and role
# every entry may contain a string or a list of strings
#
# title at organisation
# example usage: research scientist
Title :
# role at organisation
# example usage: project leader
Role :
# phone numbers
# format:
# Phone:
# type1, type2: number
# type3:
# - number1
# - number2
# custom: number
# allowed types:
# vcard 3.0: At least one of bbs, car, cell, fax, home, isdn, msg, modem,
# pager, pcs, pref, video, voice, work
# vcard 4.0: At least one of home, work, pref, text, voice, fax, cell, video,
# pager, textphone
# Alternatively you may use a single custom label (only letters).
# But beware, that not all address book clients will support custom labels.
Phone :
cell :
home :
# email addresses
# format like phone numbers above
# allowed types:
# vcard 3.0: At least one of home, internet, pref, work, x400
# vcard 4.0: At least one of home, internet, pref, work
# Alternatively you may use a single custom label (only letters).
Email :
home :
work :
# post addresses
# allowed types:
# vcard 3.0: At least one of dom, intl, home, parcel, postal, pref, work
# vcard 4.0: At least one of home, pref, work
# Alternatively you may use a single custom label (only letters).
Address :
home :
Box :
Extended :
Street :
Code :
City :
Region :
Country :
# categories or tags
# format:
# Categories : single category
# or
# Categories :
# - category1
# - category2
Categories :
# web pages
# may contain a string or a list of strings
Webpage :
# private objects
# define your own private objects in the vcard section of your khard config file
# example:
# [vcard]
# private_objects = Jabber, Skype, Twitter
# these objects are stored with a leading "X-" before the object name in the
# vcard files.
# every entry may contain a string or a list of strings
Private :%s
# notes
# may contain a string or a list of strings
# for multi-line notes use:
# Note : |
# line one
# line two
Note :
""" % '\n'.join(formatted_private_objects))
khard-0.12.2/khard/khard.py 0000664 0000000 0000000 00000215124 13230747056 0015435 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import argparse
from email.header import decode_header
import logging
import os
import re
import subprocess
import sys
from tempfile import NamedTemporaryFile
from unidecode import unidecode
from . import helpers
from .actions import Actions
from .carddav_object import CarddavObject
from .config import Config
from .version import khard_version
config = None
def write_temp_file(text=""):
"""Create a new temporary file and write some initial text to it.
:param text: the text to write to the temp file
:type text: str
:returns: the file name of the newly created temp file
:rtype: str
"""
with NamedTemporaryFile(mode='w+t', delete=False) as tempfile:
tempfile.write(text)
return tempfile.name
def create_new_contact(address_book):
# create temp file
temp_file_name = write_temp_file(
"# create new contact\n# Address book: %s\n# Vcard version: %s\n"
"# if you want to cancel, exit without saving\n\n%s"
% (address_book.name, config.get_preferred_vcard_version(),
helpers.get_new_contact_template(
config.get_supported_private_objects())))
temp_file_creation = helpers.file_modification_date(temp_file_name)
# read temp file contents before editing
with open(temp_file_name, "r") as tf:
old_contact_template = tf.read()
while True:
# start vim to edit contact template
child = subprocess.Popen([config.editor, temp_file_name])
child.communicate()
if temp_file_creation == helpers.file_modification_date(
temp_file_name):
new_contact = None
os.remove(temp_file_name)
break
# read temp file contents after editing
with open(temp_file_name, "r") as tf:
new_contact_template = tf.read()
# try to create new contact
try:
new_contact = CarddavObject.from_user_input(
address_book, new_contact_template,
config.get_supported_private_objects(),
config.get_preferred_vcard_version(),
config.localize_dates())
except ValueError as err:
print("\n%s\n" % err)
while True:
input_string = input(
"Do you want to open the editor again (y/n)? ")
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
os.remove(temp_file_name)
sys.exit(0)
if input_string.lower() == "y":
break
else:
os.remove(temp_file_name)
break
# create carddav object from temp file
if new_contact is None \
or old_contact_template == new_contact_template:
print("Canceled")
else:
new_contact.write_to_file()
print("Creation successful\n\n%s" % new_contact.print_vcard())
def modify_existing_contact(old_contact):
# create temp file and open it with the specified text editor
temp_file_name = write_temp_file(
"# Edit contact: %s\n# Address book: %s\n# Vcard version: %s\n"
"# if you want to cancel, exit without saving\n\n%s"
% (old_contact.get_full_name(), old_contact.address_book.name,
old_contact.get_version(), old_contact.get_template()))
temp_file_creation = helpers.file_modification_date(temp_file_name)
while True:
# start editor to edit contact template
child = subprocess.Popen([config.editor, temp_file_name])
child.communicate()
if temp_file_creation == helpers.file_modification_date(
temp_file_name):
new_contact = None
os.remove(temp_file_name)
break
# read temp file contents after editing
with open(temp_file_name, "r") as tf:
new_contact_template = tf.read()
# try to create contact from user input
try:
new_contact = \
CarddavObject.from_existing_contact_with_new_user_input(
old_contact, new_contact_template,
config.localize_dates())
except ValueError as err:
print("\n%s\n" % err)
while True:
input_string = input(
"Do you want to open the editor again (y/n)? ")
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
os.remove(temp_file_name)
sys.exit(0)
if input_string.lower() == "y":
break
else:
os.remove(temp_file_name)
break
# check if the user changed anything
if new_contact is None \
or old_contact == new_contact:
print("Nothing changed\n\n%s" % old_contact.print_vcard())
else:
new_contact.write_to_file(overwrite=True)
print("Modification successful\n\n%s" % new_contact.print_vcard())
def merge_existing_contacts(source_contact, target_contact,
delete_source_contact):
# show warning, if target vcard version is not 3.0 or 4.0
if target_contact.get_version() not in config.supported_vcard_versions:
print("Warning:\nThe target contact in which to merge is based on "
"vcard version %s but khard only supports the modification of "
"vcards with version 3.0 and 4.0.\nIf you proceed, the contact "
"will be converted to vcard version %s but beware: This could "
"corrupt the contact file or cause data loss."
% (target_contact.get_version(),
config.get_preferred_vcard_version()))
while True:
input_string = input("Do you want to proceed anyway (y/n)? ")
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
sys.exit(0)
if input_string.lower() == "y":
break
# create temp files for each vcard
# source vcard
source_temp_file_name = write_temp_file(
"# merge from %s\n# Address book: %s\n# Vcard version: %s\n"
"# if you want to cancel, exit without saving\n\n%s"
% (source_contact.get_full_name(), source_contact.address_book.name,
source_contact.get_version(), source_contact.get_template()))
# target vcard
target_temp_file_name = write_temp_file(
"# merge into %s\n# Address book: %s\n# Vcard version: %s\n"
"# if you want to cancel, exit without saving\n\n%s"
% (target_contact.get_full_name(), target_contact.address_book.name,
target_contact.get_version(), target_contact.get_template()))
target_temp_file_creation = helpers.file_modification_date(
target_temp_file_name)
while True:
# start editor to edit contact template
child = subprocess.Popen([config.merge_editor, source_temp_file_name,
target_temp_file_name])
child.communicate()
if target_temp_file_creation == helpers.file_modification_date(
target_temp_file_name):
merged_contact = None
os.remove(source_temp_file_name)
os.remove(target_temp_file_name)
break
# load target template contents
with open(target_temp_file_name, "r") as target_tf:
merged_contact_template = target_tf.read()
# try to create contact from user input
try:
merged_contact = \
CarddavObject.from_existing_contact_with_new_user_input(
target_contact, merged_contact_template,
config.localize_dates())
except ValueError as err:
print("\n%s\n" % err)
while True:
input_string = input(
"Do you want to open the editor again (y/n)? ")
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
os.remove(source_temp_file_name)
os.remove(target_temp_file_name)
sys.exit(0)
if input_string.lower() == "y":
break
else:
os.remove(source_temp_file_name)
os.remove(target_temp_file_name)
break
# compare them
if merged_contact is None or target_contact == merged_contact:
print("Target contact unmodified\n\n%s" % target_contact.print_vcard())
sys.exit(0)
while True:
if delete_source_contact:
input_string = input(
"Merge contact %s from address book %s into contact %s from "
"address book %s\n\nTo be removed\n\n%s\n\nMerged\n\n%s\n\n"
"Are you sure? (y/n): " % (
source_contact.get_full_name(),
source_contact.address_book.name,
merged_contact.get_full_name(),
merged_contact.address_book.name,
source_contact.print_vcard(),
merged_contact.print_vcard()))
else:
input_string = input(
"Merge contact %s from address book %s into contact %s from "
"address book %s\n\nKeep unchanged\n\n%s\n\nMerged:\n\n%s\n\n"
"Are you sure? (y/n): " % (
source_contact.get_full_name(),
source_contact.address_book.name,
merged_contact.get_full_name(),
merged_contact.address_book.name,
source_contact.print_vcard(),
merged_contact.print_vcard()))
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
return
if input_string.lower() == "y":
break
# save merged_contact to disk and delete source contact
merged_contact.write_to_file(overwrite=True)
if delete_source_contact:
source_contact.delete_vcard_file()
print("Merge successful\n\n%s" % merged_contact.print_vcard())
def copy_contact(contact, target_address_book, delete_source_contact):
source_contact_filename = ""
if delete_source_contact:
# if source file should be moved, get its file location to delete after
# successful movement
source_contact_filename = contact.filename
if not delete_source_contact \
or not contact.get_uid():
# if copy contact or contact has no uid yet
# create a new uid
contact.delete_vcard_object("UID")
contact.add_uid(helpers.get_random_uid())
# set destination file name
contact.filename = os.path.join(target_address_book.path,
"%s.vcf" % contact.get_uid())
# save
contact.write_to_file()
# delete old file
if os.path.isfile(source_contact_filename):
os.remove(source_contact_filename)
print("%s contact %s from address book %s to %s" % (
"Moved" if delete_source_contact else "Copied",
contact.get_full_name(),
contact.address_book.name,
target_address_book.name))
def list_address_books(address_book_list):
table = [["Index", "Address book"]]
for index, address_book in enumerate(address_book_list):
table.append([index+1, address_book.name])
print(helpers.pretty_print(table))
def list_contacts(vcard_list):
selected_address_books = []
for contact in vcard_list:
if contact.address_book not in selected_address_books:
selected_address_books.append(contact.address_book)
table = []
# table header
if len(selected_address_books) == 1:
print("Address book: %s" % str(selected_address_books[0]))
table_header = ["Index", "Name", "Phone", "E-Mail"]
else:
print("Address books: %s" % ', '.join(
[str(book) for book in selected_address_books]))
table_header = ["Index", "Name", "Phone", "E-Mail", "Address book"]
if config.has_uids():
table_header.append("UID")
table.append(table_header)
# table body
for index, vcard in enumerate(vcard_list):
row = []
row.append(index+1)
if vcard.get_nicknames() \
and config.show_nicknames():
if config.display_by_name() == "first_name":
row.append("%s (Nickname: %s)" % (
vcard.get_first_name_last_name(),
vcard.get_nicknames()[0]))
else:
row.append("%s (Nickname: %s)" % (
vcard.get_last_name_first_name(),
vcard.get_nicknames()[0]))
else:
if config.display_by_name() == "first_name":
row.append(vcard.get_first_name_last_name())
else:
row.append(vcard.get_last_name_first_name())
if vcard.get_phone_numbers().keys():
phone_dict = vcard.get_phone_numbers()
first_type = sorted(phone_dict.keys(),
key=lambda k: k[0].lower())[0]
row.append("%s: %s" % (first_type,
sorted(phone_dict.get(first_type))[0]))
else:
row.append("")
if vcard.get_email_addresses().keys():
email_dict = vcard.get_email_addresses()
first_type = sorted(email_dict.keys(),
key=lambda k: k[0].lower())[0]
row.append("%s: %s" % (first_type,
sorted(email_dict.get(first_type))[0]))
else:
row.append("")
if len(selected_address_books) > 1:
row.append(vcard.address_book.name)
if config.has_uids():
if config.get_shortened_uid(vcard.get_uid()):
row.append(config.get_shortened_uid(vcard.get_uid()))
else:
row.append("")
table.append(row)
print(helpers.pretty_print(table))
def list_birthdays(birthday_list):
table = [["Name", "Birthday"]]
for row in birthday_list:
table.append(row.split("\t"))
print(helpers.pretty_print(table))
def list_phone_numbers(phone_number_list):
table = [["Name", "Type", "Phone"]]
for row in phone_number_list:
table.append(row.split("\t"))
print(helpers.pretty_print(table))
def list_email_addresses(email_address_list):
table = [["Name", "Type", "E-Mail"]]
for row in email_address_list:
table.append(row.split("\t"))
print(helpers.pretty_print(table))
def choose_address_book_from_list(header_string, address_book_list):
if not address_book_list:
return None
elif len(address_book_list) == 1:
return address_book_list[0]
else:
print(header_string)
list_address_books(address_book_list)
while True:
try:
input_string = input("Enter Index: ")
if input_string in ["", "q", "Q"]:
print("Canceled")
sys.exit(0)
addr_index = int(input_string)
if addr_index > 0:
# make sure the address book is loaded afterwards
selected_address_book = config.get_address_book(
address_book_list[addr_index-1].name)
else:
raise ValueError
except (EOFError, IndexError, ValueError):
print("Please enter an index value between 1 and %d or nothing"
" to exit." % len(address_book_list))
else:
break
print("")
return selected_address_book
def choose_vcard_from_list(header_string, vcard_list):
if vcard_list.__len__() == 0:
return None
elif vcard_list.__len__() == 1:
return vcard_list[0]
else:
print(header_string)
list_contacts(vcard_list)
while True:
try:
input_string = input("Enter Index: ")
if input_string in ["", "q", "Q"]:
print("Canceled")
sys.exit(0)
addr_index = int(input_string)
if addr_index > 0:
selected_vcard = vcard_list[addr_index-1]
else:
raise ValueError
except (EOFError, IndexError, ValueError):
print("Please enter an index value between 1 and %d or nothing"
" to exit." % len(vcard_list))
else:
break
print("")
return selected_vcard
def get_contact_list_by_user_selection(address_books, search, strict_search):
"""returns a list of CarddavObject objects
:param address_books: list of selected address books
:type address_books: list(AddressBook)
:param search: filter contact list
:type search: str
:param strict_search: if True, search only in full name field
:type strict_search: bool
:returns: list of CarddavObject objects
:rtype: list(CarddavObject)
"""
return get_contacts(
address_books, search, "name" if strict_search else "all",
config.reverse(), config.group_by_addressbook(), config.sort)
def get_contacts(address_books, query, method="all", reverse=False,
group=False, sort="first_name"):
"""Get a list of contacts from one or more address books.
:param address_books: the address books to search
:type address_books: list(address_book.AddressBook)
:param query: a search query to select contacts
:type quer: str
:param method: the search method, one of "all", "name" or "uid"
:type method: str
:param reverse: reverse the order of the returned contacts
:type reverse: bool
:param group: group results by address book
:type group: bool
:param sort: the field to use for sorting, one of "first_name", "last_name"
:type sort: str
:returns: contacts from the address_books that match the query
:rtype: list(CarddavObject)
"""
# Search for the contacts in all address books.
contacts = []
for address_book in address_books:
contacts.extend(address_book.search(query, method=method))
# Sort the contacts.
if group:
if sort == "first_name":
return sorted(contacts, reverse=reverse, key=lambda x: (
unidecode(x.address_book.name).lower(),
unidecode(x.get_first_name_last_name()).lower()))
elif sort == "last_name":
return sorted(contacts, reverse=reverse, key=lambda x: (
unidecode(x.address_book.name).lower(),
unidecode(x.get_last_name_first_name()).lower()))
else:
raise ValueError('sort must be "first_name" or "last_name" not '
'{}.'.format(sort))
else:
if sort == "first_name":
return sorted(contacts, reverse=reverse, key=lambda x:
unidecode(x.get_first_name_last_name()).lower())
elif sort == "last_name":
return sorted(contacts, reverse=reverse, key=lambda x:
unidecode(x.get_last_name_first_name()).lower())
else:
raise ValueError('sort must be "first_name" or "last_name" not '
'{}.'.format(sort))
def new_subcommand(selected_address_books, input_from_stdin_or_file,
open_editor):
"""Create a new contact.
:param selected_address_books: a list of addressbooks that were selected on
the command line
:type selected_address_books: list of address_book.AddressBook
:param input_from_stdin_or_file: the data for the new contact as a yaml
formatted string
:type input_from_stdin_or_file: str
:param open_editor: whether to open the new contact in the edior after
creation
:type open_editor: bool
:returns: None
:rtype: None
"""
# ask for address book, in which to create the new contact
selected_address_book = choose_address_book_from_list(
"Select address book for new contact", selected_address_books)
if selected_address_book is None:
print("Error: address book list is empty")
sys.exit(1)
# if there is some data in stdin
if input_from_stdin_or_file:
# create new contact from stdin
try:
new_contact = CarddavObject.from_user_input(
selected_address_book, input_from_stdin_or_file,
config.get_supported_private_objects(),
config.get_preferred_vcard_version(),
config.localize_dates())
except ValueError as err:
print(err)
sys.exit(1)
else:
new_contact.write_to_file()
if open_editor:
modify_existing_contact(new_contact)
else:
print("Creation successful\n\n%s" % new_contact.print_vcard())
else:
create_new_contact(selected_address_book)
def add_email_subcommand(input_from_stdin_or_file, selected_address_books):
"""Add a new email address to contacts, creating new contacts if necessary.
:param input_from_stdin_or_file: the input text to search for the new email
:type input_from_stdin_or_file: str
:param selected_address_books: the addressbooks that were selected on the
command line
:type selected_address_books: list of address_book.AddressBook
:returns: None
:rtype: None
"""
# get name and email address
email_address = ""
name = ""
for line in input_from_stdin_or_file.splitlines():
if line.startswith("From:"):
try:
name = line[6:line.index("<")-1]
email_address = line[line.index("<")+1:line.index(">")]
except ValueError:
email_address = line[6:].strip()
break
print("Khard: Add email address to contact")
if not email_address:
print("Found no email address")
sys.exit(1)
print("Email address: %s" % email_address)
if not name:
name = input("Contact's name: ")
else:
# remove quotes from name string, otherwise decoding fails
name = name.replace("\"", "")
# fix encoding of senders name
name, encoding = decode_header(name)[0]
if encoding:
name = name.decode(encoding).replace("\"", "")
# query user input.
user_input = input("Contact's name [%s]: " % name)
# if empty, use the extracted name from above
name = user_input or name
# search for an existing contact
selected_vcard = choose_vcard_from_list(
"Select contact for the found e-mail address",
get_contact_list_by_user_selection(selected_address_books, name, True))
if selected_vcard is None:
# create new contact
while True:
input_string = input("Contact %s does not exist. Do you want "
"to create it (y/n)? " % name)
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
sys.exit(0)
if input_string.lower() == "y":
break
# ask for address book, in which to create the new contact
selected_address_book = choose_address_book_from_list(
"Select address book for new contact",
config.get_all_address_books())
if selected_address_book is None:
print("Error: address book list is empty")
sys.exit(1)
# ask for name and organisation of new contact
while True:
first_name = input("First name: ")
last_name = input("Last name: ")
organisation = input("Organisation: ")
if not first_name and not last_name and not organisation:
print("Error: All fields are empty.")
else:
break
selected_vcard = CarddavObject.from_user_input(
selected_address_book,
"First name : %s\nLast name : %s\nOrganisation : %s" % (
first_name, last_name, organisation),
config.get_supported_private_objects(),
config.get_preferred_vcard_version(),
config.localize_dates())
# check if the contact already contains the email address
for type, email_list in sorted(
selected_vcard.get_email_addresses().items(),
key=lambda k: k[0].lower()):
for email in email_list:
if email == email_address:
print("The contact %s already contains the email address %s" %
(selected_vcard, email_address))
sys.exit(0)
# ask for confirmation again
while True:
input_string = input(
"Do you want to add the email address %s to the contact %s (y/n)? "
% (email_address, selected_vcard.get_full_name()))
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
sys.exit(0)
if input_string.lower() == "y":
break
# ask for the email label
print("\nAdding email address %s to contact %s\n"
"Enter email label\n"
" vcard 3.0: At least one of home, internet, pref, work, x400\n"
" vcard 4.0: At least one of home, internet, pref, work\n"
" Or a custom label (only letters" %
(email_address, selected_vcard))
while True:
label = input("email label [internet]: ") or "internet"
try:
selected_vcard.add_email_address(label, email_address)
except ValueError as err:
print(err)
else:
break
# save to disk
selected_vcard.write_to_file(overwrite=True)
print("Done.\n\n%s" % selected_vcard.print_vcard())
def birthdays_subcommand(vcard_list, parsable):
"""Print birthday contact table.
:param vcard_list: the vcards to search for matching entries which should
be printed
:type vcard_list: list of carddav_object.CarddavObject
:param parsable: machine readable output: columns devided by tabulator (\t)
:type parsable: bool
:returns: None
:rtype: None
"""
# filter out contacts without a birthday date
vcard_list = [
vcard for vcard in vcard_list if vcard.get_birthday() is not None]
# sort by date (month and day)
vcard_list.sort(
key=lambda x: (x.get_birthday().month, x.get_birthday().day))
# add to string list
birthday_list = []
for vcard in vcard_list:
date = vcard.get_birthday()
if parsable:
if config.display_by_name() == "first_name":
birthday_list.append("%04d.%02d.%02d\t%s"
% (date.year, date.month, date.day,
vcard.get_first_name_last_name()))
else:
birthday_list.append("%04d.%02d.%02d\t%s"
% (date.year, date.month, date.day,
vcard.get_last_name_first_name()))
else:
if config.display_by_name() == "first_name":
birthday_list.append("%s\t%s"
% (vcard.get_first_name_last_name(),
vcard.get_formatted_birthday()))
else:
birthday_list.append("%s\t%s"
% (vcard.get_last_name_first_name(),
vcard.get_formatted_birthday()))
if birthday_list:
if parsable:
print('\n'.join(birthday_list))
else:
list_birthdays(birthday_list)
else:
if not parsable:
print("Found no birthdays")
sys.exit(1)
def phone_subcommand(search_terms, vcard_list, parsable):
"""Print a phone application friendly contact table.
:param search_terms: used as search term to filter the contacts before
printing
:type search_terms: str
:param vcard_list: the vcards to search for matching entries which should
be printed
:type vcard_list: list of carddav_object.CarddavObject
:param parsable: machine readable output: columns devided by tabulator (\t)
:type parsable: bool
:returns: None
:rtype: None
"""
all_phone_numbers_list = []
matching_phone_number_list = []
for vcard in vcard_list:
for type, number_list in sorted(vcard.get_phone_numbers().items(),
key=lambda k: k[0].lower()):
for number in sorted(number_list):
if config.display_by_name() == "first_name":
name = vcard.get_first_name_last_name()
else:
name = vcard.get_last_name_first_name()
# create output lines
line_formatted = "\t".join([name, type, number])
line_parsable = "\t".join([number, name, type])
if parsable:
# parsable option: start with phone number
phone_number_line = line_parsable
else:
# else: start with name
phone_number_line = line_formatted
if re.search(search_terms,
"%s\n%s" % (line_formatted, line_parsable),
re.IGNORECASE | re.DOTALL):
matching_phone_number_list.append(phone_number_line)
elif len(re.sub("\D", "", search_terms)) >= 3:
# The user likely searches for a phone number cause the
# search string contains at least three digits. So we
# remove all non-digit chars from the phone number field
# and match against that.
if re.search(re.sub("\D", "", search_terms), \
re.sub("\D", "", number), re.IGNORECASE):
matching_phone_number_list.append(phone_number_line)
# collect all phone numbers in a different list as fallback
all_phone_numbers_list.append(phone_number_line)
if matching_phone_number_list:
if parsable:
print('\n'.join(matching_phone_number_list))
else:
list_phone_numbers(matching_phone_number_list)
elif all_phone_numbers_list:
if parsable:
print('\n'.join(all_phone_numbers_list))
else:
list_phone_numbers(all_phone_numbers_list)
else:
if not parsable:
print("Found no phone numbers")
sys.exit(1)
def email_subcommand(search_terms, vcard_list, parsable, remove_first_line):
"""Print a mail client friendly contacts table that is compatible with the
default format used by mutt.
Output format:
single line of text
email_address\tname\ttype
email_address\tname\ttype
[...]
:param search_terms: used as search term to filter the contacts before
printing
:type search_terms: str
:param vcard_list: the vcards to search for matching entries which should
be printed
:type vcard_list: list of carddav_object.CarddavObject
:param parsable: machine readable output: columns devided by tabulator (\t)
:type parsable: bool
:param remove_first_line: remove first line (searching for '' ...)
:type remove_first_line: bool
:returns: None
:rtype: None
"""
matching_email_address_list = []
all_email_address_list = []
for vcard in vcard_list:
for type, email_list in sorted(vcard.get_email_addresses().items(),
key=lambda k: k[0].lower()):
for email in sorted(email_list):
if config.display_by_name() == "first_name":
name = vcard.get_first_name_last_name()
else:
name = vcard.get_last_name_first_name()
# create output lines
line_formatted = "\t".join([name, type, email])
line_parsable = "\t".join([email, name, type])
if parsable:
# parsable option: start with email address
email_address_line = line_parsable
else:
# else: start with name
email_address_line = line_formatted
if re.search(search_terms,
"%s\n%s" % (line_formatted, line_parsable),
re.IGNORECASE | re.DOTALL):
matching_email_address_list.append(email_address_line)
# collect all email addresses in a different list as fallback
all_email_address_list.append(email_address_line)
if matching_email_address_list:
if parsable:
if not remove_first_line:
# at least mutt requires that line
print("searching for '%s' ..." % search_terms)
print('\n'.join(matching_email_address_list))
else:
list_email_addresses(matching_email_address_list)
elif all_email_address_list:
if parsable:
if not remove_first_line:
# at least mutt requires that line
print("searching for '%s' ..." % search_terms)
print('\n'.join(all_email_address_list))
else:
list_email_addresses(all_email_address_list)
else:
if not parsable:
print("Found no email addresses")
elif not remove_first_line:
print("searching for '%s' ..." % search_terms)
sys.exit(1)
def list_subcommand(vcard_list, parsable):
"""Print a user friendly contacts table.
:param vcard_list: the vcards to print
:type vcard_list: list of carddav_object.CarddavObject
:param parsable: machine readable output: columns devided by tabulator (\t)
:type parsable: bool
:returns: None
:rtype: None
"""
if not vcard_list:
if not parsable:
print("Found no contacts")
sys.exit(1)
elif parsable:
contact_line_list = []
for vcard in vcard_list:
if config.display_by_name() == "first_name":
name = vcard.get_first_name_last_name()
else:
name = vcard.get_last_name_first_name()
contact_line_list.append(
'\t'.join(
[
config.get_shortened_uid(vcard.get_uid()),
name,
vcard.address_book.name
]
)
)
print('\n'.join(contact_line_list))
else:
list_contacts(vcard_list)
def modify_subcommand(selected_vcard, input_from_stdin_or_file, open_editor):
"""Modify a contact in an external editor.
:param selected_vcard: the contact to modify
:type selected_vcard: carddav_object.CarddavObject
:param input_from_stdin_or_file: new data from stdin (or a file) that
should be incorperated into the contact, this should be a yaml
formatted string
:type input_from_stdin_or_file: str
:param open_editor: whether to open the new contact in the edior after
creation
:type open_editor: bool
:returns: None
:rtype: None
"""
# show warning, if vcard version of selected contact is not 3.0 or 4.0
if selected_vcard.get_version() not in config.supported_vcard_versions:
print("Warning:\nThe selected contact is based on vcard version %s "
"but khard only supports the creation and modification of vcards"
" with version 3.0 and 4.0.\nIf you proceed, the contact will be"
" converted to vcard version %s but beware: This could corrupt "
"the contact file or cause data loss."
% (selected_vcard.get_version(),
config.get_preferred_vcard_version()))
while True:
input_string = input("Do you want to proceed anyway (y/n)? ")
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
sys.exit(0)
if input_string.lower() == "y":
break
# if there is some data in stdin
if input_from_stdin_or_file:
# create new contact from stdin
try:
new_contact = \
CarddavObject.from_existing_contact_with_new_user_input(
selected_vcard, input_from_stdin_or_file,
config.localize_dates())
except ValueError as err:
print(err)
sys.exit(1)
if selected_vcard == new_contact:
print("Nothing changed\n\n%s" % new_contact.print_vcard())
else:
print("Modification\n\n%s\n" % new_contact.print_vcard())
while True:
input_string = input("Do you want to proceed (y/n)? ")
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
break
if input_string.lower() == "y":
new_contact.write_to_file(overwrite=True)
if open_editor:
modify_existing_contact(new_contact)
else:
print("Done")
break
else:
modify_existing_contact(selected_vcard)
def remove_subcommand(selected_vcard, force):
"""Remove a contact from the addressbook.
:param selected_vcard: the contact to delete
:type selected_vcard: carddav_object.CarddavObject
:param force: delete without confirmation
:type force: bool
:returns: None
:rtype: None
"""
if not force:
while True:
input_string = input(
"Deleting contact %s from address book %s. Are you sure? (y/n): "
% (selected_vcard.get_full_name(),
selected_vcard.address_book.name))
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
sys.exit(0)
if input_string.lower() == "y":
break
selected_vcard.delete_vcard_file()
print("Contact %s deleted successfully" % selected_vcard.get_full_name())
def source_subcommand(selected_vcard, editor):
"""Open the vcard file for a contact in an external editor.
:param selected_vcard: the contact to edit
:type selected_vcard: carddav_object.CarddavObject
:param editor: the eitor command to use
:type editor: str
:returns: None
:rtype: None
"""
child = subprocess.Popen([editor, selected_vcard.filename])
child.communicate()
def merge_subcommand(vcard_list, selected_address_books, search_terms,
target_uid):
"""Merge two contacts into one.
:param vcard_list: the vcards from which to choose contacts for mergeing
:type vcard_list: list of carddav_object.CarddavObject
:param selected_address_books: the addressbooks to use to find the target
contact
:type selected_address_books: list(addressbook.AddressBook)
:param search_terms: the search terms to find the target contact
:type search_terms: str
:param target_uid: the uid of the target contact or empty
:type target_uid: str
:returns: None
:rtype: None
"""
# Check arguments.
if target_uid != "" and search_terms != "":
print("You can not specify a target uid and target search terms for a "
"merge.")
sys.exit(1)
# Find possible target contacts.
if target_uid != "":
target_vcards = get_contacts(selected_address_books, target_uid,
method="uid")
# We require that the uid given can uniquely identify a contact.
if len(target_vcards) != 1:
if not target_vcards:
print("Found no contact for target uid %s" % target_uid)
else:
print("Found multiple contacts for target uid %s" % target_uid)
for vcard in target_vcards:
print(" %s: %s" % (vcard.get_full_name(),
vcard.get_uid()))
sys.exit(1)
else:
target_vcards = get_contact_list_by_user_selection(
selected_address_books, search_terms, False)
# get the source vcard, from which to merge
source_vcard = choose_vcard_from_list("Select contact from which to merge",
vcard_list)
if source_vcard is None:
print("Found no source contact for merging")
sys.exit(1)
else:
print("Merge from %s from address book %s\n\n"
% (source_vcard.get_full_name(),
source_vcard.address_book.name))
# get the target vcard, into which to merge
target_vcard = choose_vcard_from_list("Select contact into which to merge",
target_vcards)
if target_vcard is None:
print("Found no target contact for merging")
sys.exit(1)
else:
print("Merge into %s from address book %s\n\n"
% (target_vcard.get_full_name(),
target_vcard.address_book.name))
# merging
if source_vcard == target_vcard:
print("The selected contacts are already identical")
else:
merge_existing_contacts(source_vcard, target_vcard, True)
def copy_or_move_subcommand(action, vcard_list, target_address_book_list):
"""Copy or move a contact to a different address book.
:action: the string "copy" or "move" to indicate what to do
:type action: str
:param vcard_list: the contact list from which to select one for the action
:type vcard_list: list of carddav_object.CarddavObject
:param target_address_book_list: the list of target address books
:type target_address_book_list: list(addressbook.AddressBook)
:returns: None
:rtype: None
"""
# get the source vcard, which to copy or move
source_vcard = choose_vcard_from_list(
"Select contact to %s" % action.title(), vcard_list)
if source_vcard is None:
print("Found no contact")
sys.exit(1)
else:
print("%s contact %s from address book %s"
% (action.title(), source_vcard.get_full_name(),
source_vcard.address_book.name))
# get target address book
if len(target_address_book_list) == 1 \
and target_address_book_list[0] == source_vcard.address_book:
print("The address book %s already contains the contact %s"
% (source_vcard.get_full_name(),
target_address_book_list[0].name))
sys.exit(1)
else:
available_address_books = []
for address_book in target_address_book_list:
if address_book != source_vcard.address_book:
available_address_books.append(address_book)
selected_target_address_book = choose_address_book_from_list(
"Select target address book", available_address_books)
if selected_target_address_book is None:
print("Error: address book list is empty")
sys.exit(1)
# check if a contact already exists in the target address book
target_vcard = choose_vcard_from_list(
"Select target contact which to overwrite",
get_contact_list_by_user_selection([selected_target_address_book],
source_vcard.get_full_name(), True))
# If the target contact doesn't exist, move or copy the source contact into
# the target address book without further questions.
if target_vcard is None:
copy_contact(source_vcard, selected_target_address_book,
action == "move")
else:
if source_vcard == target_vcard:
# source and target contact are identical
print("Target contact: %s" % target_vcard)
if action == "move":
copy_contact(source_vcard, selected_target_address_book, True)
else:
print("The selected contacts are already identical")
else:
# source and target contacts are different
# either overwrite the target one or merge into target contact
print("The address book %s already contains the contact %s\n\n"
"Source\n\n%s\n\nTarget\n\n%s\n\n"
"Possible actions:\n"
" a: %s anyway\n"
" m: Merge from source into target contact\n"
" o: Overwrite target contact\n"
" q: Quit"
% (
target_vcard.address_book.name,
source_vcard.get_full_name(), source_vcard.print_vcard(),
target_vcard.print_vcard(),
"Move" if action == "move" else "Copy"))
while True:
input_string = input("Your choice: ")
if input_string.lower() == "a":
copy_contact(source_vcard, selected_target_address_book,
action == "move")
break
if input_string.lower() == "o":
copy_contact(source_vcard, selected_target_address_book,
action == "move")
target_vcard.delete_vcard_file()
break
if input_string.lower() == "m":
merge_existing_contacts(source_vcard, target_vcard,
action == "move")
break
if input_string.lower() in ["", "q"]:
print("Canceled")
break
def parse_args():
"""Parse the command line arguments and return the namespace that was
creates by argparse.ArgumentParser.parse_args().
:returns: the namespace parsed from the command line
:rtype: argparse.Namespace
"""
# Create the base argument parser. It will be reused for the first and
# second round of argument parsing.
base = argparse.ArgumentParser(
description="Khard is a carddav address book for the console",
formatter_class=argparse.RawTextHelpFormatter, add_help=False)
base.add_argument("-c", "--config", default="", help="config file to use")
base.add_argument("--debug", action="store_true",
help="enable debug output")
base.add_argument("--skip-unparsable", action="store_true",
help="skip unparsable vcard files")
base.add_argument("-v", "--version", action="version",
version="Khard version %s" % khard_version)
# Create the first argument parser. Its main job is to set the correct
# config file. The config file is needed to get the default command if no
# subcommand is given on the command line. This parser will ignore most
# arguments, as they will be parsed by the second parser.
first_parser = argparse.ArgumentParser(parents=[base])
first_parser.add_argument('remainder', nargs=argparse.REMAINDER)
# Create the main argument parser. It will handle the complete command
# line only ignoring the config and debug options as these have already
# been set.
parser = argparse.ArgumentParser(parents=[base])
# create address book subparsers with different help texts
default_addressbook_parser = argparse.ArgumentParser(add_help=False)
default_addressbook_parser.add_argument(
"-a", "--addressbook", default=[],
type=lambda x: [y.strip() for y in x.split(",")],
help="Specify one or several comma separated address book names to "
"narrow the list of contacts")
new_addressbook_parser = argparse.ArgumentParser(add_help=False)
new_addressbook_parser.add_argument(
"-a", "--addressbook", default=[],
type=lambda x: [y.strip() for y in x.split(",")],
help="Specify address book in which to create the new contact")
copy_move_addressbook_parser = argparse.ArgumentParser(add_help=False)
copy_move_addressbook_parser.add_argument(
"-a", "--addressbook", default=[],
type=lambda x: [y.strip() for y in x.split(",")],
help="Specify one or several comma separated address book names to "
"narrow the list of contacts")
copy_move_addressbook_parser.add_argument(
"-A", "--target-addressbook", default=[],
type=lambda x: [y.strip() for y in x.split(",")],
help="Specify target address book in which to copy / move the "
"selected contact")
merge_addressbook_parser = argparse.ArgumentParser(add_help=False)
merge_addressbook_parser.add_argument(
"-a", "--addressbook", default=[],
type=lambda x: [y.strip() for y in x.split(",")],
help="Specify one or several comma separated address book names to "
"narrow the list of source contacts")
merge_addressbook_parser.add_argument(
"-A", "--target-addressbook", default=[],
type=lambda x: [y.strip() for y in x.split(",")],
help="Specify one or several comma separated address book names to "
"narrow the list of target contacts")
# create input file subparsers with different help texts
email_header_input_file_parser = argparse.ArgumentParser(add_help=False)
email_header_input_file_parser.add_argument(
"-i", "--input-file", default="-",
help="Specify input email header file name or use stdin by default")
template_input_file_parser = argparse.ArgumentParser(add_help=False)
template_input_file_parser.add_argument(
"-i", "--input-file", default="-",
help="Specify input template file name or use stdin by default")
template_input_file_parser.add_argument(
"--open-editor", action="store_true", help="Open the default text "
"editor after successful creation of new contact")
# create sort subparser
sort_parser = argparse.ArgumentParser(add_help=False)
sort_parser.add_argument(
"-d", "--display", choices=("first_name", "last_name"),
help="Display names in contact table by first or last name")
sort_parser.add_argument(
"-g", "--group-by-addressbook", action="store_true",
help="Group contact table by address book")
sort_parser.add_argument(
"-r", "--reverse", action="store_true",
help="Reverse order of contact table")
sort_parser.add_argument(
"-s", "--sort", choices=("first_name", "last_name"),
help="Sort contact table by first or last name")
# create search subparsers
default_search_parser = argparse.ArgumentParser(add_help=False)
default_search_parser.add_argument(
"-f", "--search-in-source-files", action="store_true",
help="Look into source vcf files to speed up search queries in "
"large address books. Beware that this option could lead "
"to incomplete results.")
default_search_parser.add_argument(
"-e", "--strict-search", action="store_true",
help="narrow contact search to name field")
default_search_parser.add_argument(
"-u", "--uid", default="", help="select contact by uid")
default_search_parser.add_argument(
"search_terms", nargs="*", metavar="search terms",
help="search in all fields to find matching contact")
merge_search_parser = argparse.ArgumentParser(add_help=False)
merge_search_parser.add_argument(
"-f", "--search-in-source-files", action="store_true",
help="Look into source vcf files to speed up search queries in "
"large address books. Beware that this option could lead "
"to incomplete results.")
merge_search_parser.add_argument(
"-e", "--strict-search", action="store_true",
help="narrow contact search to name fields")
merge_search_parser.add_argument(
"-t", "--target-contact", "--target", default="",
help="search in all fields to find matching target contact")
merge_search_parser.add_argument(
"-u", "--uid", default="", help="select source contact by uid")
merge_search_parser.add_argument(
"-U", "--target-uid", default="", help="select target contact by uid")
merge_search_parser.add_argument(
"source_search_terms", nargs="*", metavar="source",
help="search in all fields to find matching source contact")
# create subparsers for actions
subparsers = parser.add_subparsers(dest="action")
list_parser = subparsers.add_parser(
"list",
aliases=Actions.get_alias_list_for_action("list"),
parents=[default_addressbook_parser, default_search_parser,
sort_parser],
description="list all (selected) contacts",
help="list all (selected) contacts")
list_parser.add_argument(
"-p", "--parsable", action="store_true",
help="Machine readable format: uid\\tcontact_name\\taddress_book_name")
subparsers.add_parser(
"details",
aliases=Actions.get_alias_list_for_action("details"),
parents=[default_addressbook_parser, default_search_parser,
sort_parser],
description="display detailed information about one contact",
help="display detailed information about one contact")
export_parser = subparsers.add_parser(
"export",
aliases=Actions.get_alias_list_for_action("export"),
parents=[default_addressbook_parser, default_search_parser,
sort_parser],
description="export a contact to the custom yaml format that is "
"also used for editing and creating contacts",
help="export a contact to the custom yaml format that is also "
"used for editing and creating contacts")
export_parser.add_argument(
"--empty-contact-template", action="store_true",
help="Export an empty contact template")
export_parser.add_argument(
"-o", "--output-file", default=sys.stdout,
type=argparse.FileType("w"),
help="Specify output template file name or use stdout by default")
birthdays_parser = subparsers.add_parser(
"birthdays",
aliases=Actions.get_alias_list_for_action("birthdays"),
parents=[default_addressbook_parser, default_search_parser],
description="list birthdays (sorted by month and day)",
help="list birthdays (sorted by month and day)")
birthdays_parser.add_argument(
"-d", "--display", choices=("first_name", "last_name"),
help="Display names in birthdays table by first or last name")
birthdays_parser.add_argument(
"-p", "--parsable", action="store_true",
help="Machine readable format: name\\tdate")
email_parser = subparsers.add_parser(
"email",
aliases=Actions.get_alias_list_for_action("email"),
parents=[default_addressbook_parser, default_search_parser,
sort_parser],
description="list email addresses",
help="list email addresses")
email_parser.add_argument(
"-p", "--parsable", action="store_true",
help="Machine readable format: address\\tname\\ttype")
email_parser.add_argument(
"--remove-first-line", action="store_true",
help="remove \"searching for '' ...\" line from parsable output "
"(that line is required by mutt)")
phone_parser = subparsers.add_parser(
"phone",
aliases=Actions.get_alias_list_for_action("phone"),
parents=[default_addressbook_parser, default_search_parser,
sort_parser],
description="list phone numbers",
help="list phone numbers")
phone_parser.add_argument(
"-p", "--parsable", action="store_true",
help="Machine readable format: number\\tname\\ttype")
subparsers.add_parser(
"source",
aliases=Actions.get_alias_list_for_action("source"),
parents=[default_addressbook_parser, default_search_parser,
sort_parser],
description="edit the vcard file of a contact directly",
help="edit the vcard file of a contact directly")
new_parser = subparsers.add_parser(
"new",
aliases=Actions.get_alias_list_for_action("new"),
parents=[new_addressbook_parser, template_input_file_parser],
description="create a new contact",
help="create a new contact")
new_parser.add_argument(
"--vcard-version", choices=("3.0", "4.0"),
help="Select preferred vcard version for new contact")
add_email_parser = subparsers.add_parser(
"add-email",
aliases=Actions.get_alias_list_for_action("add-email"),
parents=[default_addressbook_parser, email_header_input_file_parser,
default_search_parser, sort_parser],
description="Extract email address from the \"From:\" field of an "
"email header and add to an existing contact or create a new one",
help="Extract email address from the \"From:\" field of an email "
"header and add to an existing contact or create a new one")
add_email_parser.add_argument(
"--vcard-version", choices=("3.0", "4.0"),
help="Select preferred vcard version for new contact")
subparsers.add_parser(
"merge",
aliases=Actions.get_alias_list_for_action("merge"),
parents=[merge_addressbook_parser, merge_search_parser, sort_parser],
description="merge two contacts",
help="merge two contacts")
subparsers.add_parser(
"modify",
aliases=Actions.get_alias_list_for_action("modify"),
parents=[default_addressbook_parser, template_input_file_parser,
default_search_parser, sort_parser],
description="edit the data of a contact",
help="edit the data of a contact")
subparsers.add_parser(
"copy",
aliases=Actions.get_alias_list_for_action("copy"),
parents=[copy_move_addressbook_parser, default_search_parser,
sort_parser],
description="copy a contact to a different addressbook",
help="copy a contact to a different addressbook")
subparsers.add_parser(
"move",
aliases=Actions.get_alias_list_for_action("move"),
parents=[copy_move_addressbook_parser, default_search_parser,
sort_parser],
description="move a contact to a different addressbook",
help="move a contact to a different addressbook")
remove_parser = subparsers.add_parser(
"remove",
aliases=Actions.get_alias_list_for_action("remove"),
parents=[default_addressbook_parser, default_search_parser,
sort_parser],
description="remove a contact",
help="remove a contact")
remove_parser.add_argument(
"--force", action="store_true",
help="Remove contact without confirmation")
subparsers.add_parser(
"addressbooks",
aliases=Actions.get_alias_list_for_action("addressbooks"),
description="list addressbooks",
help="list addressbooks")
# Replace the print_help method of the first parser with the print_help
# method of the main parser. This makes it possible to have the first
# parser handle the help option so that command line help can be printed
# without parsing the config file first (which is a problem if there are
# errors in the config file). The config file will still be parsed before
# the full command line is parsed so errors in the config file might be
# reported before command line syntax errors.
first_parser.print_help = parser.print_help
# Parese the command line with the first argument parser. It will handle
# the config option (its main job) and also the help, version and debug
# options as these do not depend on anything else.
args = first_parser.parse_args()
remainder = args.remainder
# Set the loglevel to debug if given on the command line. This is done
# before parsing the config file to make it possible to debug the parsing
# of the config file.
if "debug" in args and args.debug:
logging.basicConfig(level=logging.DEBUG)
# Create the global config instance.
global config
config = Config(args.config)
# Check the log level again and merge the value from the command line with
# the config file.
if ("debug" in args and args.debug) or config.debug:
logging.basicConfig(level=logging.DEBUG)
logging.debug("first args={}".format(args))
logging.debug("remainder={}".format(remainder))
# Set the default command from the config file if none was given on the
# command line.
if not remainder or \
remainder[0] not in Actions.get_all_actions_and_aliases():
remainder.insert(0, config.default_action)
logging.debug("updated remainder={}".format(remainder))
# Parse the remainder of the command line. All options from the previous
# run have already been processed and are not needed any more.
args = parser.parse_args(remainder)
logging.debug("second args={}".format(args))
return args
def main():
args = parse_args()
# if args.action isn't one of the defined actions, it must be an alias
if args.action not in Actions.get_list_of_all_actions():
# convert alias to corresponding action
# example: "ls" --> "list"
args.action = Actions.get_action_for_alias(args.action)
# display by name: first or last name
if "display" in args and args.display:
config.set_display_by_name(args.display)
# group by address book
if "group_by_addressbook" in args and args.group_by_addressbook:
config.set_group_by_addressbook(True)
# reverse contact list
if "reverse" in args and args.reverse:
config.set_reverse(True)
# sort criteria: first or last name
if "sort" in args and args.sort:
config.sort = args.sort
# preferred vcard version
if "vcard_version" in args and args.vcard_version:
config.set_preferred_vcard_version(args.vcard_version)
# search in source files
if "search_in_source_files" in args and args.search_in_source_files:
config.set_search_in_source_files(True)
# skip unparsable vcards
if "skip_unparsable" in args and args.skip_unparsable:
config.set_skip_unparsable(True)
# get all possible search queries for address book parsing
search_query_list = []
if "source_search_terms" in args and args.source_search_terms:
escaped_term = ".*".join(
[re.escape(x) for x in args.source_search_terms])
search_query_list.append(escaped_term)
args.source_search_terms = ".*%s.*" % escaped_term
if "search_terms" in args and args.search_terms:
escaped_term = ".*".join(
[re.escape(x) for x in args.search_terms])
search_query_list.append(escaped_term)
args.search_terms = ".*%s.*" % escaped_term
if "target_contact" in args and args.target_contact:
escaped_term = re.escape(args.target_contact)
search_query_list.append(escaped_term)
args.target_contact = ".*%s.*" % escaped_term
if "uid" in args and args.uid:
search_query_list.append(args.uid)
if "target_uid" in args and args.target_uid:
search_query_list.append(args.target_uid)
# create regexp
search_queries = None
if search_query_list:
search_queries = "^.*(%s).*$" % ')|('.join(search_query_list)
# load address books
if "addressbook" in args and args.addressbook != []:
# load address books which are defined in the configuration file
for index, name in enumerate(args.addressbook):
address_book = config.get_address_book(name, search_queries)
if address_book is None:
print("Error: The entered address book \"%s\" does not exist."
"\nPossible values are: %s" % (
name, ', '.join([str(book) for book in
config.get_all_address_books()])))
sys.exit(1)
else:
args.addressbook[index] = address_book
else:
# load contacts of all address books
args.addressbook = []
for address_book in config.get_all_address_books():
args.addressbook.append(config.get_address_book(
address_book.name, search_queries))
logging.debug("addressbooks: {}".format(args.addressbook))
# load target address books
if "target_addressbook" in args and args.target_addressbook != []:
for index, name in enumerate(args.target_addressbook):
address_book = config.get_address_book(name, search_queries)
if address_book is None:
print("Error: The entered address book \"%s\" does not exist."
"\nPossible values are: %s" % (
name, ', '.join([str(book) for book in
config.get_all_address_books()])))
sys.exit(1)
else:
args.target_addressbook[index] = address_book
else:
args.target_addressbook = []
for address_book in config.get_all_address_books():
args.target_addressbook.append(config.get_address_book(
address_book.name, search_queries))
logging.debug("target addressbooks: {}".format(args.target_addressbook))
# fill contact list
vcard_list = []
if "uid" in args and args.uid:
# If an uid was given we use it to find the contact.
logging.debug("args.uid={}".format(args.uid))
# We require that no search terms where given.
if ("search_terms" in args and args.search_terms) \
or ("source_search_terms" in args and args.source_search_terms):
print("Error: You can not give arbitrary search terms and "
"-uid at the same time.")
sys.exit(1)
else:
# set search terms to the empty query to prevent errors in
# phone and email actions
args.search_terms = ".*"
vcard_list = get_contacts(args.addressbook, args.uid, method="uid")
# We require that the uid given can uniquely identify a contact.
if len(vcard_list) != 1:
if not vcard_list:
print("Found no contact for %suid %s" % (
"source " if args.action == "merge" else "", args.uid))
else:
print("Found multiple contacts for %suid %s" % (
"source " if args.action == "merge" else "", args.uid))
for vcard in vcard_list:
print(" %s: %s" % (vcard.get_full_name(),
vcard.get_uid()))
sys.exit(1)
else:
# No uid was given so we try to use the search terms to select a
# contact.
if hasattr(args, "source_search_terms"):
# exception for merge command
if args.source_search_terms:
args.search_terms = args.source_search_terms
else:
args.search_terms = ".*"
elif hasattr(args, "search_terms"):
if args.search_terms:
args.search_terms = args.search_terms
else:
args.search_terms = ".*"
else:
# If no search terms where given on the command line we match
# everything with the empty search pattern.
args.search_terms = ".*"
logging.debug("args.search_terms={}".format(args.search_terms))
vcard_list = get_contact_list_by_user_selection(
args.addressbook, args.search_terms,
args.strict_search if "strict_search" in args else False)
# read from template file or stdin if available
input_from_stdin_or_file = ""
if hasattr(args, "input_file"):
if args.input_file != "-":
# try to read from specified input file
try:
with open(args.input_file, "r") as f:
input_from_stdin_or_file = f.read()
except IOError as err:
print("Error: %s\n File: %s" % (err.strerror,
err.filename))
sys.exit(1)
elif not sys.stdin.isatty():
# try to read from stdin
try:
input_from_stdin_or_file = sys.stdin.read()
except IOError:
print("Error: Can't read from stdin")
sys.exit(1)
# try to reopen console
# otherwise further user interaction is not possible (for example
# selecting a contact from the contact table)
try:
sys.stdin = open('/dev/tty')
except IOError:
pass
if args.action == "new":
new_subcommand(args.addressbook, input_from_stdin_or_file,
args.open_editor)
elif args.action == "add-email":
add_email_subcommand(input_from_stdin_or_file, args.addressbook)
elif args.action == "birthdays":
birthdays_subcommand(vcard_list, args.parsable)
elif args.action == "phone":
phone_subcommand(args.search_terms, vcard_list, args.parsable)
elif args.action == "email":
email_subcommand(args.search_terms, vcard_list,
args.parsable, args.remove_first_line)
elif args.action == "list":
list_subcommand(vcard_list, args.parsable)
elif args.action == "export" \
and "empty_contact_template" in args \
and args.empty_contact_template:
# export empty template must work without selecting a contact first
args.output_file.write(
"# Contact template for khard version %s\n#\n"
"# Use this yaml formatted template to create a new contact:\n"
"# either with: khard new -a address_book -i template.yaml\n"
"# or with: cat template.yaml | khard new -a address_book\n"
"\n%s" % (khard_version, helpers.get_new_contact_template(
config.get_supported_private_objects())))
elif args.action in ["details", "modify", "remove", "source", "export"]:
selected_vcard = choose_vcard_from_list(
"Select contact for %s action" % args.action.title(), vcard_list)
if selected_vcard is None:
print("Found no contact")
sys.exit(1)
if args.action == "details":
print(selected_vcard.print_vcard())
elif args.action == "export":
args.output_file.write(
"# Contact template for khard version %s\n"
"# Name: %s\n# Vcard version: %s\n\n%s"
% (khard_version, selected_vcard.get_full_name(),
selected_vcard.get_version(),
selected_vcard.get_template()))
elif args.action == "modify":
modify_subcommand(selected_vcard, input_from_stdin_or_file,
args.open_editor)
elif args.action == "remove":
remove_subcommand(selected_vcard, args.force)
elif args.action == "source":
source_subcommand(selected_vcard, config.editor)
elif args.action == "merge":
merge_subcommand(vcard_list, args.target_addressbook,
args.target_contact, args.target_uid)
elif args.action in ["copy", "move"]:
copy_or_move_subcommand(
args.action, vcard_list, args.target_addressbook)
elif args.action == "addressbooks":
print('\n'.join(str(book) for book in config.get_all_address_books()))
khard-0.12.2/khard/object_type.py 0000664 0000000 0000000 00000000226 13230747056 0016646 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from enum import Enum
class ObjectType(Enum):
string = 1
list_with_strings = 2
string_or_list_with_strings = 3
khard-0.12.2/khard/version.py 0000664 0000000 0000000 00000000031 13230747056 0016016 0 ustar 00root root 0000000 0000000 khard_version = '0.12.2'
khard-0.12.2/misc/ 0000775 0000000 0000000 00000000000 13230747056 0013627 5 ustar 00root root 0000000 0000000 khard-0.12.2/misc/davcontroller/ 0000775 0000000 0000000 00000000000 13230747056 0016505 5 ustar 00root root 0000000 0000000 khard-0.12.2/misc/davcontroller/davcontroller.py 0000664 0000000 0000000 00000012340 13230747056 0021735 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python2
# -*- coding: utf-8 -*-
import sys
import argparse
from caldavclientlibrary.client.account import CalDAVAccount
from caldavclientlibrary.protocol.url import URL
def main():
# create the args parser
parser = argparse.ArgumentParser(
description="Davcontroller creates, lists and removes caldav "
"calendars and carddav address books from server")
parser.add_argument("-v", "--version", action="store_true",
help="Get current program version")
parser.add_argument("-H", "--hostname", default="")
parser.add_argument("-p", "--port", default="")
parser.add_argument("-u", "--username", default="")
parser.add_argument("-P", "--password", default="")
parser.add_argument("action", nargs="?", default="",
help="Actions: new-addressbook, new-calendar, list, "
"and remove")
args = parser.parse_args()
# version
if args.version is True:
print("davcontroller version 0.1")
sys.exit(0)
# check the server's parameter
if args.hostname == "":
print("Missing host name")
sys.exit(1)
if args.port == "":
print("Missing host port")
sys.exit(1)
if args.username == "":
print("Missing user name")
sys.exit(1)
if args.password == "":
print("Missing password")
sys.exit(1)
if args.action == "":
print("Please specify an action. Possible values are: "
"new-addressbook, new-calendar, list and remove")
sys.exit(1)
elif args.action not in ["new-addressbook", "new-calendar", "list",
"remove"]:
print("The specified action \"%s\" is not supported. Possible values "
"are: new-addressbook, new-calendar, list and remove" %
args.action)
sys.exit(1)
# try to connect to the caldav server
account = CalDAVAccount(args.hostname, args.port, ssl=True,
user=args.username, pswd=args.password, root="/",
principal="")
if account.getPrincipal() is None:
print("Error: Connection refused")
sys.exit(2)
if args.action in ["list", "remove"]:
# address books
print("Available address books")
addressbook_list = account.getPrincipal().listAddressBooks()
if addressbook_list.__len__() == 0:
print("No address books found")
else:
for index, addressbook in enumerate(addressbook_list):
print("%d. %s" % (index+1, addressbook.getDisplayName()))
print()
# calendars
print("Available calendars")
calendar_list = account.getPrincipal().listCalendars()
if calendar_list.__len__() == 0:
print("No calendars found")
else:
for index, calendar in enumerate(calendar_list):
print("%d. %s" % (addressbook_list.__len__() + index + 1,
calendar.getDisplayName()))
item_list = addressbook_list + calendar_list
if item_list.__len__() == 0:
sys.exit(2)
if args.action == "remove":
print()
while True:
input_string = input("Enter Id: ")
if input_string == "":
sys.exit(0)
try:
id = int(input_string)
if id > 0 and id <= item_list.__len__():
break
except ValueError:
pass
print("Please enter an Id between 1 and %d or nothing to exit."
% item_list.__len__())
item = item_list[id-1]
while True:
input_string = input("Deleting %s. Are you sure? (y/n): " %
item.getDisplayName())
if input_string.lower() in ["", "n", "q"]:
print("Canceled")
sys.exit(0)
if input_string.lower() == "y":
break
account.session.deleteResource(URL(url=item.path))
if args.action.startswith("new-"):
# get full host url
host_url = "https://%s:%s" % (account.session.server,
account.session.port)
# enter new name
if args.action == "new-addressbook":
input_string = input("Enter new address book name or nothing to "
"cancel: ")
else:
input_string = input("Enter new calendar name or nothing to "
"cancel: ")
if input_string == "":
sys.exit(0)
res_name = input_string
res_path = input_string.replace(" ", "_").lower()
# create new resource
if args.action == "new-addressbook":
u = URL(url=host_url + account.principal.adbkhomeset[0].path +
res_path + "/")
account.session.makeAddressBook(u, res_name)
else:
u = URL(url=host_url + account.principal.homeset[0].path +
res_path + "/")
account.session.makeCalendar(u, res_name)
print("Creation successful")
if __name__ == "__main__":
main()
khard-0.12.2/misc/khard/ 0000775 0000000 0000000 00000000000 13230747056 0014720 5 ustar 00root root 0000000 0000000 khard-0.12.2/misc/khard/khard.conf.example 0000664 0000000 0000000 00000002345 13230747056 0020316 0 ustar 00root root 0000000 0000000 # example configuration file for khard version >= 0.11.0
# place it under $HOME/.config/khard/khard.conf
[addressbooks]
[[family]]
path = ~/.contacts/family/
[[friends]]
path = ~/.contacts/friends/
[general]
debug = no
default_action = list
editor = vim
merge_editor = vimdiff
[contact table]
# display names by first or last name: first_name / last_name
display = first_name
# group by address book: yes / no
group_by_addressbook = no
# reverse table ordering: yes / no
reverse = no
# append nicknames to name column: yes / no
show_nicknames = no
# show uid table column: yes / no
show_uids = yes
# sort by first or last name: first_name / last_name
sort = last_name
# localize dates: yes / no
localize_dates = yes
[vcard]
# extend contacts with your own private objects
# these objects are stored with a leading "X-" before the object name in the vcard files
# every object label may only contain letters, digits and the - character
# example:
# private_objects = Jabber, Skype, Twitter
private_objects = Jabber, Skype, Twitter
# preferred vcard version: 3.0 / 4.0
preferred_version = 3.0
# Look into source vcf files to speed up search queries: yes / no
search_in_source_files = no
# skip unparsable vcard files: yes / no
skip_unparsable = no
khard-0.12.2/misc/khard/template_for_contact_creation.yaml 0000664 0000000 0000000 00000006145 13230747056 0023672 0 ustar 00root root 0000000 0000000 # Contact template for khard version 0.11.4
#
# Use this yaml formatted template to create a new contact:
# either with: khard new -a address_book -i template.yaml
# or with: cat template.yaml | khard new -a address_book
# name components
# every entry may contain a string or a list of strings
# format:
# First name : name1
# Additional :
# - name2
# - name3
# Last name : name4
Prefix :
First name :
Additional :
Last name :
Suffix :
# nickname
# may contain a string or a list of strings
Nickname :
# important dates
# Formats:
# vcard 3.0 and 4.0: yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS
# vcard 4.0 only: --mm-dd or text= string value
# anniversary
Anniversary :
# birthday
Birthday :
# organisation
# format:
# Organisation : company
# or
# Organisation :
# - company1
# - company2
# or
# Organisation :
# -
# - company
# - unit
Organisation :
# organisation title and role
# every entry may contain a string or a list of strings
#
# title at organisation
# example usage: research scientist
Title :
# role at organisation
# example usage: project leader
Role :
# phone numbers
# format:
# Phone:
# type1, type2: number
# type3:
# - number1
# - number2
# custom: number
# allowed types:
# vcard 3.0: At least one of bbs, car, cell, fax, home, isdn, msg, modem,
# pager, pcs, pref, video, voice, work
# vcard 4.0: At least one of home, work, pref, text, voice, fax, cell, video,
# pager, textphone
# Alternatively you may use a single custom label (only letters).
# But beware, that not all address book clients will support custom labels.
Phone :
cell :
home :
# email addresses
# format like phone numbers above
# allowed types:
# vcard 3.0: At least one of home, internet, pref, work, x400
# vcard 4.0: At least one of home, internet, pref, work
# Alternatively you may use a single custom label (only letters).
Email :
home :
work :
# post addresses
# allowed types:
# vcard 3.0: At least one of dom, intl, home, parcel, postal, pref, work
# vcard 4.0: At least one of home, pref, work
# Alternatively you may use a single custom label (only letters).
Address :
home :
Box :
Extended :
Street :
Code :
City :
Region :
Country :
# categories or tags
# format:
# Categories : single category
# or
# Categories :
# - category1
# - category2
Categories :
# web pages
# may contain a string or a list of strings
Webpage :
# private objects
# define your own private objects in the vcard section of your khard config file
# example:
# [vcard]
# private_objects = Jabber, Skype, Twitter
# these objects are stored with a leading "X-" before the object name in the
# vcard files.
# every entry may contain a string or a list of strings
Private :
Jabber :
Skype :
Twitter :
# notes
# may contain a string or a list of strings
# for multi-line notes use:
# Note : |
# line one
# line two
Note :
khard-0.12.2/misc/sdiff/ 0000775 0000000 0000000 00000000000 13230747056 0014722 5 ustar 00root root 0000000 0000000 khard-0.12.2/misc/sdiff/sdiff_khard_wrapper.sh 0000664 0000000 0000000 00000000733 13230747056 0021265 0 ustar 00root root 0000000 0000000 #!/bin/sh
# khard requires the second file in the diff-operation to be modified in order to recognize a
# successful merge. However the file provided to sdiff's "-o" switch cannot be any of the source files.
#
# If you want to use sdiff to merge contacts you must set this wrapper script as your merge
# editor in khard's config file
#
# merge_editor = /path/to/sdiff_khard_wrapper.sh
FIRST="$1"
SECOND=`mktemp`
mv -f "$2" "$SECOND" && sdiff "${FIRST}" "${SECOND}" -o "$2"
khard-0.12.2/misc/twinkle/ 0000775 0000000 0000000 00000000000 13230747056 0015304 5 ustar 00root root 0000000 0000000 khard-0.12.2/misc/twinkle/scripts/ 0000775 0000000 0000000 00000000000 13230747056 0016773 5 ustar 00root root 0000000 0000000 khard-0.12.2/misc/twinkle/scripts/config.py 0000664 0000000 0000000 00000001421 13230747056 0020610 0 ustar 00root root 0000000 0000000 #!/usr/bin/python
# -*- coding: utf-8 -*-
import os
# twinkle config folder
twinkle_config = os.path.join(os.environ['HOME'], ".twinkle")
# khard executable
khard_exe = os.path.join(os.environ['HOME'], ".virtualenvs", "bin", "khard")
# user language
language = "de"
# stop mpd
stop_music = True
mpd_host = "192.168.2.100"
mpd_port = 6600
# log file for calls
call_log_file = os.path.join(twinkle_config, "calls.log")
# audio files
constant_ringtone_segment = os.path.join(twinkle_config, "sounds", "ringtone_segment.wav")
new_ringtone = os.path.join(twinkle_config, "sounds", "special_ringtone.wav")
# temp files
tmp_mono_file = "/tmp/caller_id.wav"
tmp_file_stereo = "/tmp/caller_id_stereo.wav"
mpd_lockfile = "/tmp/mpd_stopped"
caller_id_filename = "/tmp/current_caller_id"
khard-0.12.2/misc/twinkle/scripts/incoming_call.py 0000775 0000000 0000000 00000007175 13230747056 0022160 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# This script speaks the incoming caller ID for the SIP client Twinkle
# The following programs are needed: espeak, ffmpeg and sox, mpc is optional
# aptitude install ffmpeg espeak sox mpc
# Further information about Twinkle scripts can be found at
# http://mfnboer.home.xs4all.nl/twinkle/manual.html#profile_scripts
import os, subprocess, sys, re
import config
def get_caller_id(from_hdr):
caller_id = from_hdr[from_hdr.find(":")+1:from_hdr.find("@")]
# remove all non digits from caller id
caller_id = re.sub("\D", "", caller_id)
# remove two digit country identification if present
if not caller_id.startswith("0"):
return caller_id[2:]
return caller_id
def caller_from_addressbook(caller_id):
try:
callers = subprocess.check_output([config.khard_exe, "phone", "--parsable", caller_id]).strip()
except subprocess.CalledProcessError:
return caller_id
if len(callers.split("\n")) == 1:
return callers.split("\t")[1]
else:
# the contact contains multiple phone numbers and we have to obtain the right phone label
regexp = re.compile(caller_id, re.IGNORECASE)
for entry in callers.split("\n"):
if regexp.search(re.sub("\D", "", entry.split("\t")[0])) != None:
return "%s (%s)" % (entry.split("\t")[1], entry.split("\t")[2])
return callers.split("\n")[0].split("\t")[1]
def create_ringtone(caller_id):
if os.path.exists(config.new_ringtone) == True:
os.remove(config.new_ringtone)
if config.language == "de":
subprocess.call(["espeak", "-v", "de", "-s", "300", "-w", config.tmp_mono_file, caller_id])
else:
subprocess.call(["espeak", "-v", "en-us", "-s", "300", "-w", config.tmp_mono_file, caller_id])
subprocess.call(["ffmpeg", "-i", config.tmp_mono_file, "-ar", "48000", "-ac", "2", "-y", config.tmp_file_stereo],
stdout=open('/dev/null', 'w'), stderr=open('/dev/null', 'w'))
subprocess.call(["sox", config.constant_ringtone_segment, config.tmp_file_stereo, config.new_ringtone])
# main part of the script
if os.path.exists(config.constant_ringtone_segment) == False:
print("The constant part of the ringtone file is missing. Create the sounds folder in your twinkle config and put a wav file in it")
sys.exit(1)
# pause the music playback
# I use a MPD server for playing music so I pause it with the client MPC
# You can disable that in the config.py file
if config.stop_music:
mpc_output = subprocess.check_output(["mpc", "-h", config.mpd_host, "-p", str(config.mpd_port), "status"])
if "playing" in mpc_output:
subprocess.call(["mpc", "-h", config.mpd_host, "-p", str(config.mpd_port), "pause"])
music_tmp_file = open(config.mpd_lockfile, "w")
music_tmp_file.close()
if "SIP_FROM" in os.environ:
from_hdr = os.environ["SIP_FROM"]
# parse the caller ID of the string
caller_id = get_caller_id(from_hdr)
# look into the addressbook
if caller_id != "":
caller_id = caller_from_addressbook(caller_id)
else:
caller_id = "anonymous"
# create the ringtone
if config.language == "de":
create_ringtone("Anruf von " + caller_id)
else:
create_ringtone("Call from " + caller_id)
# save the caller id for later use
with open(config.caller_id_filename, "w") as caller_id_file:
caller_id_file.write(caller_id)
# if the file creation was successful and the file exists, tell twinkle to use it as the ringtone
# else do nothing and play the standard ringtone
if os.path.exists(config.new_ringtone) == True:
print("ringtone=" + config.new_ringtone)
sys.exit()
khard-0.12.2/misc/twinkle/scripts/incoming_call_ended.py 0000775 0000000 0000000 00000002121 13230747056 0023301 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import os, subprocess, time, datetime
import config
# current date and time
datetime = datetime.datetime.now()
current_date = "%.2d.%.2d.%.4d" % (datetime.day, datetime.month, datetime.year)
current_time = "%.2d:%.2d:%.2d" % (datetime.hour, datetime.minute, datetime.second)
# if music was stopped, resume again
if os.path.exists(config.mpd_lockfile) == True:
os.remove(config.mpd_lockfile)
subprocess.call(["mpc", "-h", config.mpd_host, "-p", str(config.mpd_port), "play"])
# try to get the caller name / id from the previously created temp file
try:
caller_id_file = open(config.caller_id_filename, "r")
caller_id = caller_id_file.read().strip()
caller_id_file.close()
except:
caller_id = "anonymous"
if config.language == "de":
message = "Anruf von %s am %s um %s\n" % (caller_id, current_date, current_time)
else:
message = "Call of %s in %s at %s\n" % (caller_id, current_date, current_time)
try:
os.remove(config.caller_id_filename)
except:
pass
# log into file
log = open(config.call_log_file, "a")
log.write(message)
log.close()
khard-0.12.2/misc/twinkle/scripts/incoming_call_failed.py 0000775 0000000 0000000 00000002131 13230747056 0023447 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import os, subprocess, time, datetime
import config
# current date and time
datetime = datetime.datetime.now()
current_date = "%.2d.%.2d.%.4d" % (datetime.day, datetime.month, datetime.year)
current_time = "%.2d:%.2d:%.2d" % (datetime.hour, datetime.minute, datetime.second)
# if music was stopped, resume again
if os.path.exists(config.mpd_lockfile) == True:
os.remove(config.mpd_lockfile)
subprocess.call(["mpc", "-h", config.mpd_host, "-p", str(config.mpd_port), "play"])
# try to get the caller name / id from the previously created temp file
try:
with open(config.caller_id_filename, "r") as caller_id_file:
caller_id = caller_id_file.read().strip()
except:
caller_id = "anonymous"
if config.language == "de":
message = "Anruf in Abwesenheit von %s am %s um %s\n" % (caller_id, current_date, current_time)
else:
message = "Call in absence of %s in %s at %s\n" % (caller_id, current_date, current_time)
try:
os.remove(config.caller_id_filename)
except:
pass
# log into file
with open(config.call_log_file, "a") as log:
log.write(message)
khard-0.12.2/misc/twinkle/sounds/ 0000775 0000000 0000000 00000000000 13230747056 0016617 5 ustar 00root root 0000000 0000000 khard-0.12.2/misc/twinkle/sounds/incoming_call.wav 0000664 0000000 0000000 00022527514 13230747056 0022156 0 ustar 00root root 0000000 0000000 RIFFDJ WAVEfmt data J x~}|zz}~||{yx}t{|}tjziuw}zw|~~~~}~~zz|w~}|{}}{x|~~ ) ' . 9 3 > : L D Z U p X ` W Q U u M x I w @ r 2 r 0 o D t O H 6 ~ # c J I P
F 1 # %
3 <