pax_global_header 0000666 0000000 0000000 00000000064 14504053514 0014513 g ustar 00root root 0000000 0000000 52 comment=1bbce67fa64cafbc1b50eb3ecd54e48c797f8437
org-caldav-master/ 0000775 0000000 0000000 00000000000 14504053514 0014367 5 ustar 00root root 0000000 0000000 org-caldav-master/.gitignore 0000664 0000000 0000000 00000000007 14504053514 0016354 0 ustar 00root root 0000000 0000000 *.elc
org-caldav-master/License.txt 0000664 0000000 0000000 00000104505 14504053514 0016517 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.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
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:
{project} Copyright (C) {year} {fullname}
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
.
org-caldav-master/README.org 0000664 0000000 0000000 00000051700 14504053514 0016040 0 ustar 00root root 0000000 0000000 #+TITLE: org-caldav
Caldav sync for Emacs Orgmode
*Minimum Emacs version needed*: 26.3
CalDAV servers:
- *Owncloud* and *Nextcloud*: Regularly tested.
- *Google Calendar*: Should work, but you need to register an
application with the Google Developer Console for OAuth2
authentication (see below), because Google explicitly forbids to put
client id/secrets into open source software (see
https://developers.google.com/terms, section 4b, paragraph 1). Instead
of doing that though, I'd rather suggest you choose another service
provider.
- *Radicale* and *Baikal*: Should work. If you get problems with
'Digest' authentication, switch back to 'Basic' (make sure to use
https, though!). If you get asked for password repeatedly, put it in
~.authinfo~ file (see below).
- *SOGo* and *Kolab*: Reported to be working
(https://kolabnow.com/clients/emacs)
Note that Emacs releases <26.3 might not correctly handle https via
TLSv1.3 (see https://debbugs.gnu.org/cgi/bugreport.cgi?bug=34341). If
you see errors like "Bad request" or "No data received" you can either
try to set
#+begin_src emacs-lisp
(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
#+end_src
or you upgrade to Emacs 26.3.
*IMPORTANT*: Before using this code, please make sure you have backups
of your precious Org files. Also, I strongly suggest to create a new,
empty calendar on your server for using this package.
*ALSO IMPORTANT*: When using this package, possibly all Org entries will
get an UID property (see the docstring of ~org-icalendar-store-UID~ for
further details). If you don't want this, then /do not use this
package/; there is just no way around that. It is the only reliable way
to uniquely identify Org entries.
* IN A NUTSHELL
- Create a new calendar; the name does not matter.
- Set ~org-caldav-url~ to the base address of your CalDAV server:
- Owncloud/Nextcloud (9.x and above):
https://OWNCLOUD-SERVER-URL/remote.php/dav/calendars/USERID
- Google: Set to symbol ~'google~. See below for further documentation.
- Set ~org-caldav-calendar-id~ to the calendar ID of your new calendar:
- Own/NextCloud: Click on that little symbol next to the calendar name
and inspect the link of the calendar; the last element of the shown
path is the calendar-id. This should /usually/ be the same as the
name of the calendar, but not necessarily: Owncloud might replace
certain characters (upper to lowercase, for instance), or it might
even be entirely different if the calendar was created by another
CalDAV application.
- Google: Click on 'calendar settings' and the id will be shown next
to "Calendar Address". It is of the form
~ID@group.calendar.google.com~. Do /not/ omit the domain!
- Set ~org-caldav-inbox~ to an org filename where new entries from the
calendar should be stored. Just to be safe, I suggest using an empty,
dedicated Org file for that.
- Set ~org-caldav-files~ to the list of org files you would like to
sync. The above ~org-caldav-inbox~ will be automatically added, so you
don't have to add it here.
- It is usually a good idea to manually set ~org-icalendar-timezone~ to
the timezone of your remote calendar. It should be a simple string
like "Europe/Berlin". If that doesn't work and your events are
shifted by a few hours, try the setting "UTC" (the SOGo calendar
server seems to need this).
Please note that org-caldav does not directly control how and which
entries are exported, it just uses the org-icalendar
exporter. Therefore, you should also take a look at the options from
the org-icalendar exporter. Most importantly, take a look at
~org-icalendar-alarm-time~ to add a reminder to your entries, and
~org-icalendar-use-deadline~ and ~org-icalendar-use-scheduled~ to control
which timestamps should be used.
Call ~org-caldav-sync~ to start the sync. The ~url~ package will ask you
for username/password for accessing the calendar. (See below on how to
store that password in an authinfo file.)
The first sync can easily take several minutes, depending on the
number of calendar items. Especially Google's CalDAV interface is
pretty slow. If you have to abort the initial sync for some reason,
just start ~org-caldav-sync~ again in the same Emacs session and you
should get asked if you'd like to resume.
The same goes for sync errors you might get. Especially when using
Google Calendar, it is not unusual to get stuff like '409' errors
during the initial sync. Only Google knows why. Just run
~org-caldav-sync~ again until all events are uploaded.
* Syncing to Google Calendar
The new CalDAV endpoint for Google Calendar requires OAuth2
authentication. So first, you need to install the oauth2 library from
GNU ELPA, and afterwards you need to acquire an application ID and
secret from the Google Developer Console. For details on how to do
this, follow the Google documentation at
https://developers.google.com/google-apps/calendar/caldav/v2/guide#creating_your_client_id
Put the client ID and secret into ~org-caldav-oauth2-client-id~ and
~org-caldav-oauth2-client-secret~, respectively. Then set
~org-caldav-url~ to the symbol ~'google~, and look up the
~org-caldav-calendar-id~ as described above.
On first connection, the oauth2 library should redirect you to the
Google OAuth2 authentication site. This requires a javascript enabled
browser, so make sure that ~browse-url-browser-function~ is set to
something like ~browse-url-firefox~ (the internal eww or w3m browsers
will *not* work). After authentication, you will be given a key that
you have to paste into the Emacs prompt. The oauth2 library will save
this key in Emacs' secure plist store, which is encrypted with
GnuPG. If you have not yet used a secure plist store, you will be
asked for its encryption passphrase. In the future, you should only
need to enter that passphrase again to connect with Google Calendar.
By default, plstore will *not* cache your entered password, so it will
possibly ask you *many* times. To activate caching, use
#+begin_src emacs-lisp
(setq plstore-cache-passphrase-for-symmetric-encryption t)
#+end_src
* DETAILS
Compared to earlier versions of this package from 2012, it now does
proper two-way syncing, that means it does not matter where and how
you change an entry. You can also move Org entries freely from one
file to another, as long as they are all listed in
~org-caldav-files~. The org-icalendar package will put a unique ID on
each entry with an active timestamp, so that org-caldav can find
it. It will also sync deletions, but more on that later.
You can also return to the simpler version which only does one-way
syncing. Simply set ~org-caldav-sync-direction~ to ~'org->cal~ or
~'cal->org~, depending on which direction you'd like to have. If you
choose ~'org->cal~, then ~org-caldav-inbox~ won't matter and can be
~nil~. Likewise, if you choose ~'cal->org~, then ~org-caldav-files~
will be ignored and only the calendar will be imported into the inbox.
** Org and the iCalendar format
An Org entry can store much more information than an iCalendar entry,
so there is no one-to-one correspondence between the two formats which
makes syncing a bit difficult.
- Org to iCalendar
This package uses the org-icalendar package to do the export to the
iCalendar format (.ics files). By default, it uses the title of the
Org entry as SUMMARY and puts the entry's body into DESCRIPTION,
snipping stuff like properties and timestamps (you can override that
with properties of the same name, but IMO it makes stuff just more
complicated). The variable ~org-icalendar-include-body~
denotes how many characters from the body should be included as
DESCRIPTION (by default all characters are included).
- iCalendar to Org
If you create a new iCalendar entry in your calendar, you'll get an
Org entry with SUMMARY as heading, DESCRIPTION as body and the
timestamp. However, if you /change/ an existing entry in the calendar,
things get more complicated and the variable
~org-caldav-sync-changes-to-org~ comes into play. Its default is the
symbol "title-and-timestamp", which means that only the entry's
heading is synced (with SUMMARY) and the timestamp gets updated, but
/not/ the entry's body with DESCRIPTION. The simple reason is that
you might loose data, since DESCRIPTION is rather limited in what it
can store. Still, you can set the variable to the symbol "all", which
will completely /replace/ an existing Org entry with the entry that
gets generated from the calendar's event. You can also limit syncing
to heading and/or timestamp only.
To be extra safe, org-caldav will by default backup entries it
changes. See the variable ~org-caldav-backup-file~ for details.
- Org sexp entries
A special case are sexp entries like
#+begin_src org
%%(diary-anniversary 2 2 1969) Foo's birthday
,* Regular meeting
<%%(diary-float t 4 2)>
#+end_src
As you can see, they can appear in two different ways: plain by
themselves, or inside an Org entry. If they are inside an Org entry,
there's a good chance they will be exported (see below) and have an ID
property, so they can be found by org-caldav. We can sync the title,
but syncing the timestamp with the s-expression is just infeasible, so
this will generate a sync error (which are /not/ critical; you'll just
see them at the end of the sync, just so that you're aware that some
stuff wasn't synced properly).
However, sexp-entries are insanely flexible, and there are limits as
to what the icalendar exporter will handle. For example, this here
#+begin_src org
,** Regular event
<%%(memq (calendar-day-of-week date) '(1 3 5))>
#+end_src
will not be exported at all.
If the sexp entry is not inside an Org entry but stands by itself,
they still will be exported, but they won't get an ID (since IDs are
properties linked to Org entries). In practice, that means that you
can delete and change them inside Org and this will be synced, but if
you /change/ them in the /calendar/, this will /not/ get synced
back. Org-caldav just cannot find those entries, so this will generate
a one-time sync error instead (again: those are not critical, just
FYI). If you don't want those entries to be exported at all, just set
~org-icalendar-include-sexps~ to nil.
** Filtering entries
There are several possibilities to choose which entries should be
synced and which not:
- If you only want to sync manually marked entries, use
~org-caldav-select-tags~, which is directly mapped to
~org-export-select-tags~, so see its doc-string on how it works.
- If you want to exclude certain tags, use ~org-caldav-exclude-tags~,
which is mapped to ~org-icalendar-exclude~ tags.
- If you want more fine grained control, use
~org-caldav-skip-conditions~. The syntax of the conditions is
described in the doc-string of ~org-agenda-skip-if~.
- In case you just want to keep your remote calendar clean, set
~org-caldav-days-in-past~ to the number of days you want to keep in
the past on the remote calendar. This does not affect your org files,
it works just as a filter for entries older than N days.
Note however that the normal ~org-agenda-skip-function(-global)~ will
*not* have any effect on the icalendar exporter (this used to be the
case, but changed with the new exporters).
** Syncing deletions
If you delete entries in your Org files, the corresponding iCalendar
entries will by default get deleted. You can change that behavior with
~org-caldav-delete-calendar-entries~ to never delete, or to ask before
deletion.
You must be careful to not simply remove previously synced files from
~org-caldav-files~, as org-caldav would view all the entries from those
files as deleted and hence by default also delete them from the
calendar. However, org-caldav should be able to detect this situation
and warn you with the message 'Previously synced file(s) are missing',
asking you whether to continue nonetheless.
If you delete events in your calendar, you will by default get asked
if you'd like to delete the corresponding Org event. You can change
that behavior through ~org-caldav-delete-org-entries~.
If you answer a deletion request with "no", the event should get
re-synced to the calendar next time you call ~org-caldav-sync~.
** Conflict handling
Now that's an easy one: Org always wins. That means, if you change an
entry in Org /and/ in the calendar, the changes in the calendar will
be lost. I might implement proper conflict handling some day, but
don't hold your breath (patches are welcome, of course).
** Storing authentication information in authinfo/netrc
If you don't want to enter your user/password every time, you can
store it permanently in an authinfo file. In Emacs, the auth-source
package takes care of that, but the syntax for https authentication is
a bit peculiar. You have to use a line like the following
#+begin_example
machine www.google.com:443 port https login username password secret
#+end_example
Note that you have to specify the port number in the URL and /also/
specify 'https' for the port. This is not a bug. For more information,
see (info "auth"), especially section "Help for users".
Since you are storing your password in a file you should encrypt it
using GnuPG. Emacs will prompt you for a decryption key when it tries
to read the file.
** Storage of sync information and sync from different computers
The current sync state is stored in a file ~org-caldav-SOMEID.el~ in
the ~/.emacs.d directory. You can change the location through the
variable ~org-caldav-save-directory~. SOMEID directly depends on the
calendar id (it's a snipped MD5).
If you sync your Org files across different machines and want to use
org-caldav on all of them, don't forget to sync the org sync state,
too. Probably your best bet is to set ~org-caldav-save-directory~ to the
path you have your Org files in, so that it gets copied alongside with
them.
** Starting from scratch
If your sync state somehow gets broken, you can make a clean slate by
doing
#+begin_example
C-u M-x org-caldav-delete-everything
#+end_example
The function has to be called with a prefix so that you don't call it
by accident. This will delete everything in the calendar along with
the current sync state. You can then call ~org-caldav-sync~ afterwards
and it will completely put all Org events into the now empty
calendar. Needless to say, don't do that if you have new events in
your calendar which are not synced yet...
Deleting many events can be slow, though; in that case, just delete
the calendar and re-create it, delete the sync state file in
~/.emacs.d and restart Emacs.
** Syncing with more than one calendar
This can be done by setting the variable ~org-caldav-calendars~. It
should be a list of plists (a 'plist' is simply a list with alternating
:key's and values). Through these plists, you can override the global
values of variables like ~org-caldav-calendar-id~, and calling
~org-caldav-sync~ will go through these plists in order.
Example:
#+begin_src emacs-lisp
(setq org-caldav-calendars
'((:calendar-id "work@whatever" :files ("~/org/work.org")
:inbox "~/org/fromwork.org")
(:calendar-id "stuff@mystuff"
:files ("~/org/sports.org" "~/org/play.org")
:skip-conditions (regexp "soccer")
:inbox "~/org/fromstuff.org")) )
#+end_src
This means that you have two calendars with IDs "work@whatever" and
"stuff@mystuff". Both will be accessed through the global value of
org-caldav-url, since the key :url isn't specified. The calendar
"work@whatever" will be synced with the file 'work.org' and inbox
'fromwork.org', while "stuff@mystuff" with 'sports.org' and
'play.org', /unless/ there's the string 'soccer' in the heading, and
and inbox is 'fromstuff.org'. See the doc-string of
~org-caldav-calendars~ for more details on which keys you can use.
** Customizing the inbox
See the doc-string of ~org-caldav-inbox~ if you want more flexibility in
where new items should be put. Instead of simply providing a file, you
can also choose an existing entry or headline, or put the entry under a
datetree.
** Timezone problems
Timezone handling is plain horrible, and it seems every CalDAV server
does it slightly differently, also using non-standard headers like
X-WR-TIMEZONE. If you see items being shifted by a few hours, make
really really sure you have properly set ~org-icalendar-timezone~, and
that your calendar is configured to use the same one.
If it still does not work, you can try setting ~org-icalendar-timezone~
to the string "UTC". This will put all events using UTC times and the
server should transpose the time to the timezone you have set in your
calendar preferences. For some servers (like SOGo) this might work
better than setting a "real" timezone.
** Troubleshooting
If org-caldav reports a problem with the given URL, please
triple-check that the URL is correct. It must point to a valid
calendar on your CalDAV server.
If the error is that the URL does not seem to accept DAV requests, you
can additionally check with 'curl' by doing
#+begin_src shell
curl -D - -X OPTIONS --basic -u mylogin:mypassword URL
#+end_src
The output of this command must contain a 'DAV' header like this:
#+begin_example
DAV: 1, 3, extended-mkcol, access-control, ... etc. ...
#+end_example
By default, org-caldav will put all kinds of debug output into the
buffer ~*org-caldav-debug*~. Look there if you're getting sync errors
or if something plain doesn't work. If you're using an authinfo file
and authentication doesn't work, set auth-info-debug to t and look in
the ~*Messages*~ buffer. When you report a bug, please try to post the
relevant portion of the ~*org-caldav-debug*~ buffer since it might be
helpful to see what's going wrong. If Emacs throws an error, do
#+begin_example
M-x toggle-debug-on-error
#+end_example
and try to replicate the error to get a backtrace.
You can also turn on excessive debugging by setting the variable
~org-caldav-debug-level~ to 2. This will also output the /contents/ of
the events into the debug buffer. If you send such a buffer in a bug
report, please make very sure you have removed personal information
from those events.
** Syncing TODOs between Org and CalDav
This feature is relatively new and less well tested, so it is
recommended to have backups before using it. It has been tested on
nextcloud and radicale.
To sync TODO's between Org and the CalDav server, do:
#+begin_src emacs-lisp
(setq org-icalendar-include-todo 'all
org-caldav-sync-todo t)
#+end_src
The first instructs the Org exporter to include TODOs; the second
tells org-caldav to import icalendar VTODOs as Org TODOs.
Other customizations to consider (see their documentation for more
details):
- ~org-caldav-todo-priority~ to control how priority levels map between
iCalendar and Org.
- ~org-caldav-todo-percent-states~ to convert between
~org-todo-keywords~ and iCalendar's percent-complete property.
- ~org-caldav-todo-deadline-schedule-warning-days~ to auto-create
SCHEDULED timestamps when a DEADLINE is present (this might be useful
for users of the OpenTasks app).
If you find that some Org entries get an extra tag which equals their
CATEGORY, this might be caused by the CATEGORY being exported to
iCalendar, and then re-imported to Org as a tag. In that case, do
#+begin_src emacs-lisp
(setq org-icalendar-categories '(local-tags))
#+end_src
to prevent the CATEGORY from being exported to iCalendar. This problem
only seems to affect some CalDav servers: in particular, NextCloud
is affected, but Radicale does not seem to experience this problem.
** Known Bugs
- Recurring events created or changed on the calendar side cannot be
synced (they will work fine as long as you manage them in Org,
though).
- Syncing is currently pretty slow since everything is done
synchronously.
- Pretty much everything besides SUMMARY, DESCRIPTION, LOCATION and time
is ignored in iCalendar.
** How syncing happens (a.k.a. my little CalDAV rant)
(This is probably not interesting, so you can just stop reading.)
CalDAV is a mess.
First off, it is based on WebDAV, which has its own fair share of
problems. The main design flaw of CalDAV however, is that UID and
resource name (the "filename", if you want) are two different
things. I know that there are reasons for that (not everything has a
UID, like timezones, and you can put several events in one resource),
but this is typical over-engineering to allow some marginal use cases
pretty much no one needs. Another problem is that you have to do
additional round-trips to get Etag and sequence number, which makes
CalDAV pretty slow.
Org-caldav takes the easy route: it assumes that every resource
contains one event, and that UID and resource name are identical. In
fact, Google's CalDAV interface even enforces the latter. And while
Owncloud does not enforce it, at least it just does it if you create
items in its web interface.
However, the CalDAV standard does not demand this, so I guess there
are servers out there with which org-caldav does not work. Patches
welcome.
Now, all this would be bad enough if it weren't for the sloppy server
implementations which make implementing a CalDAV client a living hell
and led to several rewrites of the code. Especially Google, the 500
pound gorilla in the room, doesn't really care much for CalDAV. I
guess they really like their own shiny REST-based calendar API better,
and I can't blame them for that.
org-caldav-master/org-caldav-testsuite.el 0000664 0000000 0000000 00000116065 14504053514 0020770 0 ustar 00root root 0000000 0000000 ;; Test suite for org-caldav.el
;; Copyright, authorship, license: see org-caldav.el.
;; Run it from the org-caldav directory like this:
;; TZ="Europe/Berlin" emacs -Q -L . --eval '(setq org-caldav-url "CALDAV-URL")' -l org-caldav-testsuite.el -f ert
;;
;; On the server, there must already exist two calendars "test1" and "test2".
;; These will completely wiped by running this test!
;;
;; Hint: In case you need a test server, one lightweight option is:
;; docker run -v /path/to/data:/data tomsquest/docker-radicale
;; Then, you can create the test1 calendar from Thunderbird like so:
;; Thunderbird -> New Calendar -> Network -> Location:
;; http://localhost:5232/test/test1/ (the trailing slash is
;; important), with username "test" and blank password. Then add an
;; event from Thunderbird to make sure the calendar exists.
(require 'ert)
(require 'org)
(require 'org-caldav)
(require 'cl-lib)
(when (org-caldav-use-oauth2)
(org-caldav-check-oauth2 org-caldav-url)
(org-caldav-retrieve-oauth2-token org-caldav-url))
(defvar org-caldav-test-calendar-names '("test1" "test2"))
(setq org-caldav-delete-calendar-entries 'always)
(setq org-caldav-backup-file nil)
(setq org-caldav-test-preamble
"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-TIMEZONE:Europe/Berlin
X-WR-CALDESC:
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
")
;; First test event in calendar
(setq org-caldav-test-ics1
"BEGIN:VEVENT
DTSTART;VALUE=DATE:20121220
DTEND;VALUE=DATE:20121221
DTSTAMP:20121218T212132Z
UID:orgcaldavtest@cal1
CREATED:20121216T205929Z
DESCRIPTION:A first test
LAST-MODIFIED:20121218T212132Z
LOCATION:
SUMMARY:Test appointment Number 1
END:VEVENT
")
;; How it should end up in Org
(setq org-caldav-test-ics1-org
"\\* Test appointment Number 1
\\s-*:PROPERTIES:
\\s-*:ID:\\s-*orgcaldavtest@cal1
\\s-*:END:
\\s-*<2012-12-20 Thu>
\\s-*A first test")
;; Dto., second one
(setq org-caldav-test-ics2
"BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20121205T190000
DTEND;TZID=Europe/Berlin:20121205T200000
DTSTAMP:20121219T213352Z
UID:orgcaldavtest-cal2
CREATED:20121219T213352Z
DESCRIPTION:A second test
LAST-MODIFIED:20121219T213352Z
LOCATION:
SUMMARY:Test appointment Number 2
END:VEVENT
")
(setq org-caldav-test-ics2-org
"\\* Test appointment Number 2
\\s-*:PROPERTIES:
\\s-*:ID:\\s-*orgcaldavtest-cal2
\\s-*:END:
\\s-*<2012-12-05 Wed 19:00-20:00>
\\s-*A second test")
;; First test task in calendar
(setq org-caldav-test-ics3
"BEGIN:VTODO
UID:orgcaldavtest@cal3
DTSTAMP:20220828T161432Z
DTSTART;VALUE=DATE:20121223
SUMMARY:A test task from iCal
DESCRIPTION:ical test task 1
PRIORITY:0
STATUS:NEEDS-ACTION
END:VTODO
")
(setq org-caldav-test-ics3-org
"\\* TODO A test task from iCal
\\s-*SCHEDULED: <2012-12-23 Sun>
\\s-*:PROPERTIES:
\\s-*:ID: orgcaldavtest@cal3
\\s-*:END:
\\s-*ical test task 1")
;; Second test task in calendar
(setq org-caldav-test-ics4
"BEGIN:VTODO
UID:orgcaldavtest-cal4
DTSTAMP:20220828T161432Z
DTSTART;VALUE=DATE:20121219
DUE;VALUE=DATE:20121223
SUMMARY:Another test task from iCal
DESCRIPTION:ical test task 2
PRIORITY:5
STATUS:NEEDS-ACTION
END:VTODO
")
(setq org-caldav-test-ics4-org
"\\* TODO \\[#B\\] Another test task from iCal
\\s-*DEADLINE: <2012-12-23 Sun> SCHEDULED: <2012-12-19 Wed>
\\s-*:PROPERTIES:
\\s-*:ID: orgcaldavtest-cal4
\\s-*:END:
\\s-*ical test task 2")
;; First test entry in Org which should end up in calendar
(setq org-caldav-test-org1
"* This is a test
:PROPERTIES:
:ID: orgcaldavtest@org1
:END:
<2012-12-23 Sun 20:00-21:00>
Foo Bar Baz
")
;; Dto., second one
(setq org-caldav-test-org2
"* This is another test
:PROPERTIES:
:ID: orgcaldavtest-org2
:END:
<2012-12-19 Wed 19:00-21:00>
Baz Bar Foo
")
(setq org-caldav-test-org3
"* This is a test with a tag :sometag:
:PROPERTIES:
:ID: orgcaldavtest-org3
:END:
<2012-12-20 Thu 19:00-21:00>
moose
")
;; First test task in Org which should end up in calendar
(setq org-caldav-test-org4
"* TODO A test task from Org
SCHEDULED: <2012-12-23 Sun>
:PROPERTIES:
:ID: orgcaldavtest@org4
:END:
Org task 1
")
;; Dto., second one
(setq org-caldav-test-org5
"* TODO [#B] Another test task from Org
DEADLINE: <2012-12-23 Sun> SCHEDULED: <2012-12-19 Wed>
:PROPERTIES:
:ID: orgcaldavtest-org5
:END:
Org task 2
")
;; All events after sync.
(setq org-caldav-test-allevents
'("orgcaldavtest@org1" "orgcaldavtest-org2" "orgcaldavtest@cal1" "orgcaldavtest-cal2"))
(setq org-caldav-test-alltodos
'("orgcaldavtest@org4" "orgcaldavtest-org5" "orgcaldavtest@cal3" "orgcaldavtest-cal4"))
(setq org-caldav-test-sync-result
'(("test1" "orgcaldavtest@cal1" new-in-cal cal->org)
("test1" "orgcaldavtest-cal2" new-in-cal cal->org)))
;; Test files.
(defun org-caldav-test-calendar-empty-p ()
"Check if calendar is empty."
(let ((output (org-caldav-url-dav-get-properties
(org-caldav-events-url) "resourcetype")))
(unless (eq (plist-get (cdar output) 'DAV:status) 200)
(error "Could not query CalDAV URL %s: %s." (org-caldav-events-url) (prin1-to-string output)))
(= (length output) 1)))
(defun org-caldav-test-set-up ()
"Make a clean slate."
(message "SET UP")
(unless (org-caldav-test-calendar-empty-p)
(dolist (cur (org-caldav-get-event-etag-list))
(message "Deleting %s" (car cur))
(org-caldav-delete-event (car cur))))
(should (org-caldav-test-calendar-empty-p)))
(defun org-caldav-test-put-events ()
"Put initial events to calendar."
(message "PUT")
(let ((org-caldav-calendar-preamble org-caldav-test-preamble)
events)
(with-temp-buffer
(insert org-caldav-test-ics1)
(should (org-caldav-put-event (current-buffer)))
(erase-buffer)
(insert org-caldav-test-ics2)
(should (org-caldav-put-event (current-buffer))))
(should
(setq events
(org-caldav-get-event-etag-list)))
(should (assoc "orgcaldavtest@cal1" events))
(should (assoc "orgcaldavtest-cal2" events))))
(defun org-caldav-test-setup-temp-files ()
(let ((tmpdir (make-temp-file "org-caldav-test-" t)))
(message "Using tempdir %s" tmpdir)
(setq org-caldav-save-directory (expand-file-name "org-caldav-savedir" tmpdir)
org-caldav-backup-file (expand-file-name "org-caldav-test-backup.org" tmpdir)
org-caldav-test-orgfile (expand-file-name "test.org" tmpdir)
org-caldav-test-second-orgfile (expand-file-name "test-second.org" tmpdir)
org-caldav-test-inbox (expand-file-name "inbox.org" tmpdir)
org-id-locations-file (expand-file-name "org-id-locations" tmpdir)
org-id-locations nil
org-id-files nil
org-caldav-test-current-tempdir tmpdir))
(make-directory org-caldav-save-directory)
(with-current-buffer (find-file-noselect org-caldav-test-inbox)
(save-buffer)))
(defun org-caldav-test-cleanup ()
(dolist (name '("test.org" "test-second.org" "inbox.org" "org-id-locations"))
(let ((buf (get-buffer name)))
(when buf
(with-current-buffer buf
(set-buffer-modified-p nil)
(kill-buffer)))))
(setq org-id-locations nil)
(setq org-caldav-event-list nil)
(when org-caldav-test-current-tempdir
(delete-directory org-caldav-test-current-tempdir t)
(setq org-caldav-test-current-tempdir nil)))
;; This is one, big, big test, since pretty much everything depends on
;; the current calendar/org state and I cannot easily split it.
(ert-deftest org-caldav-01-sync-test ()
(message "Setting up temporary files")
(org-caldav-test-setup-temp-files)
(setq org-caldav-calendar-id (car org-caldav-test-calendar-names))
;; Set up orgfile.
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(insert org-caldav-test-org1 "\n")
(insert org-caldav-test-org2 "\n")
(save-buffer))
;; Set up data for org-caldav.
(setq org-caldav-files (list org-caldav-test-orgfile))
(setq org-caldav-inbox org-caldav-test-inbox)
(setq org-caldav-debug-level 2)
(message "Cleaning up upstream calendars")
(org-caldav-test-set-up)
(message "Putting events")
(org-caldav-test-put-events)
(message "1st SYNC")
;; Do the sync.
(org-caldav-sync)
;; Check result.
(should (member `(,org-caldav-calendar-id "orgcaldavtest@cal1" new-in-cal cal->org)
org-caldav-sync-result))
(should (member `(,org-caldav-calendar-id "orgcaldavtest-cal2" new-in-cal cal->org)
org-caldav-sync-result))
(should (member `(,org-caldav-calendar-id "orgcaldavtest@org1" new-in-org org->cal)
org-caldav-sync-result))
(should (member `(,org-caldav-calendar-id "orgcaldavtest-org2" new-in-org org->cal)
org-caldav-sync-result))
;; State file should exist now.
(should (file-exists-p
(org-caldav-sync-state-filename org-caldav-calendar-id)))
(let ((calevents (org-caldav-get-event-etag-list)))
(should (= (length calevents) (length org-caldav-test-allevents)))
;; Org events should be in cal.
(dolist (cur org-caldav-test-allevents)
(should (assoc cur calevents))))
;; Cal events should be in Org.
(with-current-buffer (find-file-noselect org-caldav-test-inbox)
(goto-char (point-min))
(should (re-search-forward org-caldav-test-ics1-org nil t))
(goto-char (point-min))
(should (re-search-forward org-caldav-test-ics2-org nil t)))
(message "2nd SYNC")
;; Sync again.
(org-caldav-sync)
;; Nothing should have happened.
(should-not org-caldav-sync-result)
(message "Changing org events")
;; Now change events in Org
(with-current-buffer (find-buffer-visiting org-caldav-test-orgfile)
(goto-char (point-min))
(search-forward "This is another test")
(replace-match "This is a changed test heading")
(search-forward "<2012-12-19 Wed 19:00-21:00>")
(replace-match "<2012-12-19 Thu 20:00-22:00>"))
(with-current-buffer (find-buffer-visiting org-caldav-test-inbox)
(goto-char (point-min))
(search-forward "Test appointment Number 2")
(replace-match "Appointment number 2 was changed!")
(search-forward "<2012-12-05 Wed 19:00-20:00>")
(replace-match "<2012-12-04 Tue 18:00-19:00>"))
;; And sync...
(message "3rd SYNC")
(org-caldav-sync)
(should (equal `((,org-caldav-calendar-id "orgcaldavtest-cal2" changed-in-org org->cal)
(,org-caldav-calendar-id "orgcaldavtest-org2" changed-in-org org->cal))
org-caldav-sync-result))
;; Check if those events correctly end up in calendar.
(with-current-buffer (org-caldav-get-event "orgcaldavtest-cal2")
(goto-char (point-min))
(save-excursion
(should (search-forward "SUMMARY:Appointment number 2 was changed!")))
(save-excursion
(should (re-search-forward "DTSTART.*:20121204T\\(170000Z\\|180000\\)" nil t)))
(save-excursion
(should (re-search-forward "DTEND.*:20121204T\\(180000Z\\|190000\\)" nil t))))
(with-current-buffer (org-caldav-get-event "orgcaldavtest-org2")
(goto-char (point-min))
(save-excursion
(should (search-forward "SUMMARY:This is a changed test heading")))
(save-excursion
(should (re-search-forward "DTSTART.*:20121219T\\(190000Z\\|200000\\)" nil t)))
(save-excursion
(should (re-search-forward "DTEND.*:20121219T\\(210000Z\\|220000\\)" nil t))))
;; Now change events in Cal
(message "Changing events in calendar")
(with-current-buffer (org-caldav-get-event "orgcaldavtest@cal1")
(goto-char (point-min))
(save-excursion
(search-forward "SUMMARY:Test appointment Number 1")
(replace-match "SUMMARY:Changed test appointment Number 1"))
(save-excursion
(re-search-forward "DTSTART\\(;.*\\)?:\\(20121220\\)")
(replace-match "20121212" nil nil nil 2))
(save-excursion
(re-search-forward "DTEND\\(;.*\\)?:\\(20121221\\)")
(replace-match "20121213" nil nil nil 2))
(save-excursion
(when (re-search-forward "SEQUENCE:\\s-*\\([0-9]+\\)" nil t)
(replace-match (number-to-string
(1+ (string-to-number (match-string 1))))
nil t nil 1)))
(message "PUTting first changed event")
(should (org-caldav-save-resource
(concat (org-caldav-events-url) (url-hexify-string "orgcaldavtest@cal1.ics"))
(encode-coding-string (buffer-string) 'utf-8))))
(with-current-buffer (org-caldav-get-event "orgcaldavtest@org1")
(goto-char (point-min))
(save-excursion
(search-forward "SUMMARY:This is a test")
(replace-match "SUMMARY:This is a changed test"))
(save-excursion
(if (re-search-forward "DTSTART\\(;.*\\)?:\\(20121223T190000Z\\)" nil t)
(replace-match "20121213T180000Z" nil nil nil 2)
(re-search-forward "DTSTART\\(;.*\\)?:\\(20121223T200000\\)")
(replace-match "20121213T190000" nil nil nil 2)))
(save-excursion
(if (re-search-forward "DTEND\\(;.*\\)?:\\(20121223T200000Z\\)" nil t)
(replace-match "20121213T190000Z" nil nil nil 2)
(re-search-forward "DTEND\\(;.*\\)?:\\(20121223T210000\\)")
(replace-match "20121213T200000" nil nil nil 2)))
(save-excursion
(when (re-search-forward "SEQUENCE:\\s-*\\([0-9]+\\)" nil t)
(replace-match (number-to-string
(1+ (string-to-number (match-string 1))))
nil t nil 1)))
(message "PUTting second changed event")
(should (org-caldav-save-resource
(concat (org-caldav-events-url) "orgcaldavtest@org1.ics")
(encode-coding-string (buffer-string) 'utf-8))))
;; Aaaand sync!
(message "4th SYNC")
(org-caldav-sync)
(should (equal `((,org-caldav-calendar-id "orgcaldavtest@cal1" changed-in-cal cal->org)
(,org-caldav-calendar-id "orgcaldavtest@org1" changed-in-cal cal->org))
org-caldav-sync-result))
(with-current-buffer (find-file-noselect org-caldav-test-inbox)
(goto-char (point-min))
(should (re-search-forward
"* Changed test appointment Number 1
\\s-*:PROPERTIES:
\\s-*:ID:\\s-*orgcaldavtest@cal1
\\s-*:END:
\\s-*<2012-12-12 Wed>
\\s-*A first test")))
(message "Deleting event in Org")
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(goto-char (point-min))
(should (re-search-forward
"* This is a changed test
\\s-*:PROPERTIES:
\\s-*:ID:\\s-*orgcaldavtest@org1
\\s-*:END:
\\s-*<2012-12-13 Thu 19:00-20:00>
\\s-*Foo Bar Baz"))
;; Delete this event in Org
(replace-match ""))
;; Sync
(message "6th SYNC")
(org-caldav-sync)
;; Event should be deleted in calendar
(let ((calevents (org-caldav-get-event-etag-list)))
(should (= (length calevents) 3))
(should-not (assoc '"orgcaldavtest@org1" calevents)))
(should
(equal org-caldav-sync-result
`((,org-caldav-calendar-id "orgcaldavtest@org1" deleted-in-org removed-from-cal))))
(should-not
(assoc '"orgcaldavtest@org1" org-caldav-event-list))
;; Delete event in calendar
(message "Delete event in calendar")
(should (org-caldav-delete-event "orgcaldavtest-org2"))
;; Sync one last time
(message "7th SYNC")
(let ((org-caldav-delete-org-entries 'always))
(org-caldav-sync))
(should
(equal org-caldav-sync-result
`((,org-caldav-calendar-id "orgcaldavtest-org2" deleted-in-cal removed-from-org))))
;; There shouldn't be anything left in that buffer
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(goto-char (point-min))
(should-not (re-search-forward "[:alnum:]" nil t)))
(should-not
(assoc '"orgcaldavtest-org2" org-caldav-event-list))
(org-caldav-test-cleanup)
)
(ert-deftest org-caldav-02-change-heading-test ()
(with-current-buffer (get-buffer-create "headingtest")
(erase-buffer)
(insert "* This is a test without timestamp in headline\n"
" <2009-08-08 Sat 10:00>\n whatever\n foo\n bar\n")
(insert "* This is a test <2009-08-08 Sat> \n"
" whatever\n foo\n bar\n")
(insert "* <2009-08-08 Sat 14:00> This is another test\n"
" whatever\n foo\n bar\n")
(org-mode)
(goto-char (point-min))
(save-excursion
(org-caldav-change-heading "first changed heading"))
(should (looking-at "^\\* first changed heading$"))
(search-forward "*" nil t 2)
(beginning-of-line)
(save-excursion
(org-caldav-change-heading "second changed heading"))
(should (looking-at "^\\* second changed heading <2009-08-08 Sat> $"))
(search-forward "*" nil t 2)
(beginning-of-line)
(save-excursion
(org-caldav-change-heading "third changed heading"))
(should (looking-at "^\\* <2009-08-08 Sat 14:00> third changed heading\n"))
))
(ert-deftest org-caldav-03-insert-org-entry ()
"Make sure that `org-caldav-insert-org-entry' works fine."
(let ((entry '("01 01 2015" "19:00" "01 01 2015" "20:00" "The summary" "The description" "location" nil))
(org-caldav-select-tags ""))
(cl-flet ((write-entry (uid level)
(with-temp-buffer
(org-mode) ; useful to set org-mode's
; internal variables
(apply #'org-caldav-insert-org-entry
(append entry (list uid level)))
(setq foo (buffer-string)))))
(should (string-match (concat
"\\*\\s-+The summary\n"
"\\s-*:PROPERTIES:\n"
"\\s-*:LOCATION: location\n"
"\\s-*:END:\n"
"\\s-*<2015-01-01 Thu 19:00-20:00>\n"
"\\s-*The description\n")
(write-entry nil nil)))
(should (string-match (concat
"\\*\\*\\s-+The summary\n"
"\\s-*:PROPERTIES:\n"
"\\s-*:LOCATION: location\n"
"\\s-*:END:\n"
"\\s-*<2015-01-01 Thu 19:00-20:00>\n"
"\\s-*The description\n")
(write-entry nil 2)))
(should (string-match (concat "\\*\\s-+The summary\n"
"\\s-*:PROPERTIES:\n"
"\\s-*:ID:\\s-*1\n"
"\\s-*:LOCATION: location\n"
"\\s-*:END:\n"
"\\s-*<2015-01-01 Thu 19:00-20:00>\n"
"\\s-*The description\n")
(write-entry "1" nil))))))
(ert-deftest org-caldav-04-multiple-calendars ()
(org-caldav-test-setup-temp-files)
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(insert org-caldav-test-org1)
(save-buffer))
(with-current-buffer (find-file-noselect org-caldav-test-second-orgfile)
(insert org-caldav-test-org2)
(save-buffer))
;; Delete calendar contents
(let ((org-caldav-calendar-id (car org-caldav-test-calendar-names)))
(org-caldav-test-set-up))
(let ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names)))
(org-caldav-test-set-up))
(let
((org-caldav-calendars
`((:calendar-id ,(car org-caldav-test-calendar-names)
:url ,org-caldav-url
:files (,org-caldav-test-orgfile)
:inbox ,org-caldav-test-orgfile)
(:calendar-id ,(nth 1 org-caldav-test-calendar-names)
:url ,org-caldav-url
:files (,org-caldav-test-second-orgfile)
:inbox ,org-caldav-test-second-orgfile))))
(org-caldav-sync))
;; Check that each calendar has one event
(let
((org-caldav-calendar-id (car org-caldav-test-calendar-names)))
(should (org-caldav-get-event "orgcaldavtest@org1"))
(should-error (org-caldav-get-event "orgcaldavtest-org2")))
(let
((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names)))
(should-error (org-caldav-get-event "orgcaldavtest@org1"))
(should (org-caldav-get-event "orgcaldavtest-org2")))
(org-caldav-test-cleanup)
)
(ert-deftest org-caldav-05-multiple-calendars-agenda-skip-function ()
(org-caldav-test-setup-temp-files)
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(insert org-caldav-test-org1)
(insert org-caldav-test-org2)
(insert org-caldav-test-org3)
(save-buffer))
;; Delete calendar contents
(let ((org-caldav-calendar-id (car org-caldav-test-calendar-names)))
(org-caldav-test-set-up))
(let ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names)))
(org-caldav-test-set-up))
(message "Starting sync")
(let
((org-caldav-calendars
`((:calendar-id ,(car org-caldav-test-calendar-names)
:url ,org-caldav-url
:skip-conditions (regexp ":sometag:")
:files (,org-caldav-test-orgfile)
:inbox ,org-caldav-test-orgfile)
(:calendar-id ,(nth 1 org-caldav-test-calendar-names)
:url ,org-caldav-url
:skip-conditions (notregexp ":sometag:")
:files (,org-caldav-test-orgfile)
:inbox ,org-caldav-test-orgfile))))
(org-caldav-sync))
;; Check that each calendar has one event
(let
((org-caldav-calendar-id (car org-caldav-test-calendar-names)))
(should (org-caldav-get-event "orgcaldavtest@org1"))
(should (org-caldav-get-event "orgcaldavtest-org2"))
(should-error (org-caldav-get-event "orgcaldavtest-org3")))
(let
((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names)))
(should (org-caldav-get-event "orgcaldavtest-org3"))
(should-error (org-caldav-get-event "orgcaldavtest@org1"))
(should-error (org-caldav-get-event "orgcaldavtest-org2")))
;; Make sure org-agenda-skip-function-global is not set permanently
(should-not org-agenda-skip-function-global)
(org-caldav-test-cleanup)
)
;; Make sure setting org-caldav-files to 'nil' does not
;; do anything weird.
(ert-deftest org-caldav-06-org-caldav-files-nil ()
(message "Setting up temporary files")
(org-caldav-test-setup-temp-files)
(setq org-caldav-calendar-id (car org-caldav-test-calendar-names))
;; Set org-caldav-files to nil
(setq org-caldav-files nil)
(setq org-caldav-inbox org-caldav-test-inbox)
(setq org-caldav-debug-level 2)
(message "Setting up upstream calendar")
(org-caldav-test-set-up)
(message "Putting events")
(org-caldav-test-put-events)
(org-caldav-sync)
;; Events must still be in calendar
(should (org-caldav-get-event "orgcaldavtest@cal1"))
(should (org-caldav-get-event "orgcaldavtest-cal2"))
;; Sync result
(should (or (equal org-caldav-test-sync-result
org-caldav-sync-result)
(equal (reverse org-caldav-test-sync-result)
org-caldav-sync-result)))
(org-caldav-test-cleanup))
;; Check that we are able to detect when an Org file was removed from
;; org-caldav-files between syncs.
(ert-deftest org-caldav-07-detect-removed-file ()
(message "Setting up temporary files")
(org-caldav-test-setup-temp-files)
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(insert org-caldav-test-org1)
(save-buffer))
(with-current-buffer (find-file-noselect org-caldav-test-second-orgfile)
(insert org-caldav-test-org2)
(save-buffer))
(setq org-caldav-calendar-id (car org-caldav-test-calendar-names))
;; Set org-caldav-files to nil
(setq org-caldav-files (list org-caldav-test-orgfile org-caldav-test-second-orgfile))
(setq org-caldav-inbox org-caldav-test-inbox)
(setq org-caldav-debug-level 2)
(message "Setting up upstream calendar")
(org-caldav-test-set-up)
(message "Putting events")
(org-caldav-test-put-events)
(message "1st sync")
(org-caldav-sync)
;; Remove one of the files
(setq org-caldav-files (list org-caldav-test-second-orgfile))
;; Sync again, binding yes-or-no-p to our test
(setq org-caldav-test-seen-prompt nil)
(let (octest-seen-prompt)
(cl-letf (((symbol-function 'yes-or-no-p)
(lambda (prompt)
(setq octest-seen-prompt prompt) nil)))
(message "2nd sync")
(should-error (org-caldav-sync))
(should (string-match "WARNING: Previously synced"
octest-seen-prompt))))
(org-caldav-test-cleanup))
(ert-deftest org-caldav-test-multiline-location ()
(with-temp-buffer
(org-mode)
(insert org-caldav-test-org1)
(goto-char (point-min))
(let ((orig-id (alist-get "ID" (org-entry-properties) nil nil #'string=)))
(org-caldav-change-location "multi\nline")
(let ((props (org-entry-properties)))
(should (string= (alist-get "ID" props nil nil #'string=) orig-id))
(should (string-match-p "multi" (alist-get "LOCATION" props nil nil #'string=)))
(should (string-match-p "line" (alist-get "LOCATION" props nil nil #'string=)))))))
(ert-deftest org-caldav-08-test-setting-sync-direction ()
(org-caldav-test-setup-temp-files)
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(insert org-caldav-test-org1)
(insert org-caldav-test-org2)
(insert org-caldav-test-org3)
(save-buffer))
;; Delete calendar contents
(let ((org-caldav-calendar-id (car org-caldav-test-calendar-names)))
(org-caldav-test-set-up))
(let ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names)))
(org-caldav-test-set-up))
(message "Starting sync")
(let
((org-caldav-calendars
;; First only syncs Org to calendar
`((:calendar-id ,(car org-caldav-test-calendar-names)
:url ,org-caldav-url
:files (,org-caldav-test-orgfile)
:inbox nil ;; No inbox needed
:sync-direction org->cal)
;; Second only syncs Calendar to Org inbox
(:calendar-id ,(nth 1 org-caldav-test-calendar-names)
:url ,org-caldav-url
:files nil ;; No files needed
:inbox ,org-caldav-test-inbox
:sync-direction cal->org))))
(org-caldav-sync)
;; First calendar should sync everything
(let
((org-caldav-calendar-id (car org-caldav-test-calendar-names)))
(should (org-caldav-get-event "orgcaldavtest@org1"))
(should (org-caldav-get-event "orgcaldavtest-org2"))
(should (org-caldav-get-event "orgcaldavtest-org3")))
;; Second calendar syncs nothing from org to cal
(let
((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names)))
(should-error (org-caldav-get-event "orgcaldavtest-org3"))
(should-error (org-caldav-get-event "orgcaldavtest@org1"))
(should-error (org-caldav-get-event "orgcaldavtest-org2")))
;; Put calendar events in both calendars
(let ((org-caldav-calendar-id (car org-caldav-test-calendar-names)))
(org-caldav-test-put-events))
(let ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names)))
(org-caldav-test-put-events))
;; Sync again
(org-caldav-sync)
;; Events are still there in first
(let
((org-caldav-calendar-id (car org-caldav-test-calendar-names)))
(should (org-caldav-get-event "orgcaldavtest@org1"))
(should (org-caldav-get-event "orgcaldavtest-org2"))
(should (org-caldav-get-event "orgcaldavtest-org3")))
;; Events still not there in second
(let
((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names)))
(should-error (org-caldav-get-event "orgcaldavtest-org3"))
(should-error (org-caldav-get-event "orgcaldavtest@org1"))
(should-error (org-caldav-get-event "orgcaldavtest-org2")))
;; But second should have new events in inbox
(with-current-buffer (find-file-noselect org-caldav-test-inbox)
(goto-char (point-min))
(should (re-search-forward org-caldav-test-ics1-org nil t))
(goto-char (point-min))
(should (re-search-forward org-caldav-test-ics2-org nil t))))
(org-caldav-test-cleanup)
)
;; Based on org-caldav-01-sync-test, but modified for todos
(ert-deftest org-caldav-09-sync-test-todo ()
(let ((org-caldav-sync-todo t)
(org-icalendar-include-todo 'all))
(message "Setting up temporary files")
(org-caldav-test-setup-temp-files)
(setq org-caldav-calendar-id (car org-caldav-test-calendar-names))
;; Set up data for org-caldav.
(setq org-caldav-files (list org-caldav-test-orgfile))
(setq org-caldav-inbox org-caldav-test-inbox)
(setq org-caldav-debug-level 2)
(message "Cleaning up upstream calendars")
(org-caldav-test-set-up)
;; Set up orgfile.
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(insert org-caldav-test-org4 "\n")
(insert org-caldav-test-org5 "\n")
(save-buffer))
(message "Putting events")
(let ((org-caldav-calendar-preamble org-caldav-test-preamble)
events)
(with-temp-buffer
(insert org-caldav-test-ics3)
(should (org-caldav-put-event (current-buffer)))
(erase-buffer)
(insert org-caldav-test-ics4)
(should (org-caldav-put-event (current-buffer))))
(let ((events (org-caldav-get-event-etag-list)))
(should (assoc "orgcaldavtest@cal3" events))
(should (assoc "orgcaldavtest-cal4" events))))
(message "1st SYNC")
;; Do the sync.
(org-caldav-sync)
;;;; Check result.
(should (= (length (org-caldav-get-event-etag-list)) 4))
(should (member `(,org-caldav-calendar-id "orgcaldavtest@cal3" new-in-cal cal->org)
org-caldav-sync-result))
(should (member `(,org-caldav-calendar-id "orgcaldavtest-cal4" new-in-cal cal->org)
org-caldav-sync-result))
(should (member `(,org-caldav-calendar-id "orgcaldavtest@org4" new-in-org org->cal)
org-caldav-sync-result))
(should (member `(,org-caldav-calendar-id "orgcaldavtest-org5" new-in-org org->cal)
org-caldav-sync-result))
;; State file should exist now.
(should (file-exists-p
(org-caldav-sync-state-filename org-caldav-calendar-id)))
(let ((calevents (org-caldav-get-event-etag-list)))
(should (= (length calevents) (length org-caldav-test-alltodos)))
;; Org events should be in cal.
(dolist (cur org-caldav-test-alltodos)
(should (assoc cur calevents))))
;; Cal events should be in Org.
(with-current-buffer (find-file-noselect org-caldav-test-inbox)
(goto-char (point-min))
(should (re-search-forward org-caldav-test-ics3-org nil t))
(goto-char (point-min))
(should (re-search-forward org-caldav-test-ics4-org nil t)))
(message "2nd SYNC")
;; Sync again.
(org-caldav-sync)
;; Nothing should have happened.
(should-not org-caldav-sync-result)
(message "Changing org events")
;; Now change events in Org
(with-current-buffer (find-buffer-visiting org-caldav-test-orgfile)
(goto-char (point-min))
(search-forward "TODO A test task from Org")
(replace-match "DONE Finished test task from Org")
(search-forward "SCHEDULED:")
(replace-match "CLOSED: [2012-12-24 Mon 00:00] SCHEDULED:" t))
(with-current-buffer (find-buffer-visiting org-caldav-test-inbox)
(goto-char (point-min))
(search-forward "TODO [#B] Another test task from iCal")
(replace-match "DONE [#C] Another test task from iCal was finished!")
(search-forward "DEADLINE:")
(replace-match "CLOSED: [2012-12-20 Thu 00:00] DEADLINE:" t))
;; And sync...
(message "3rd SYNC")
(org-caldav-sync)
(should (equal `((,org-caldav-calendar-id "orgcaldavtest-cal4" changed-in-org org->cal)
(,org-caldav-calendar-id "orgcaldavtest@org4" changed-in-org org->cal))
org-caldav-sync-result))
;; Check if those events correctly end up in calendar.
(with-current-buffer (org-caldav-get-event "orgcaldavtest-cal4")
(goto-char (point-min))
(save-excursion
(should (search-forward "SUMMARY:Another test task from iCal was finished!")))
(save-excursion
(should (search-forward "PRIORITY:9")))
(save-excursion
(should (search-forward "STATUS:COMPLETED")))
(save-excursion
(should (re-search-forward "COMPLETED.*:20121220T000000"))))
(with-current-buffer (org-caldav-get-event "orgcaldavtest@org4")
(goto-char (point-min))
(save-excursion
(should (search-forward "SUMMARY:Finished test task from Org")))
(save-excursion
(should (search-forward "PRIORITY:0")))
(save-excursion
(should (search-forward "STATUS:COMPLETED")))
(save-excursion
(should (re-search-forward "COMPLETED.*:20121224T000000"))))
;; Now change events in Cal
(message "Changing events in calendar")
(with-current-buffer (org-caldav-get-event "orgcaldavtest@cal3")
(goto-char (point-min))
(save-excursion
(search-forward "SUMMARY:A test task from iCal")
(replace-match "SUMMARY:Changed A test task from iCal"))
(save-excursion
(re-search-forward "DTSTART\\(;.*\\)?:\\(20121223\\)")
(replace-match "20121224" nil nil nil 2))
(save-excursion
(when (re-search-forward "SEQUENCE:\\s-*\\([0-9]+\\)" nil t)
(replace-match (number-to-string
(1+ (string-to-number (match-string 1))))
nil t nil 1)))
(message "PUTting first changed event")
(should (org-caldav-save-resource
(concat (org-caldav-events-url) (url-hexify-string "orgcaldavtest@cal3.ics"))
(encode-coding-string (buffer-string) 'utf-8))))
(with-current-buffer (org-caldav-get-event "orgcaldavtest-org5")
(goto-char (point-min))
(save-excursion
(search-forward "SUMMARY:Another test task from Org")
(replace-match "SUMMARY:Changed Another test task from Org"))
(save-excursion
(search-forward "STATUS:NEEDS-ACTION")
(replace-match "STATUS:COMPLETED\nCOMPLETED:20121224T000000"))
(save-excursion
(search-forward "PERCENT-COMPLETE:0")
(replace-match "PERCENT-COMPLETE:100"))
(save-excursion
(when (re-search-forward "SEQUENCE:\\s-*\\([0-9]+\\)" nil t)
(replace-match (number-to-string
(1+ (string-to-number (match-string 1))))
nil t nil 1)))
(message "PUTting second changed event")
(should (org-caldav-save-resource
(concat (org-caldav-events-url) "orgcaldavtest-org5.ics")
(encode-coding-string (buffer-string) 'utf-8))))
;; Aaaand sync!
(message "4th SYNC")
(org-caldav-sync)
(should (equal `((,org-caldav-calendar-id "orgcaldavtest@cal3" changed-in-cal cal->org)
(,org-caldav-calendar-id "orgcaldavtest-org5" changed-in-cal cal->org))
org-caldav-sync-result))
(with-current-buffer (find-file-noselect org-caldav-test-inbox)
(goto-char (point-min))
(should (re-search-forward
"* TODO Changed A test task from iCal
\\s-*SCHEDULED: <2012-12-24 Mon>
\\s-*:PROPERTIES:
\\s-*:ID:\\s-*orgcaldavtest@cal3
\\s-*:END:
\\s-*ical test task 1")))
(message "Changing description in icalendar")
(with-current-buffer (org-caldav-get-event "orgcaldavtest@cal3")
(goto-char (point-min))
(save-excursion
(search-forward "DESCRIPTION:ical test task 1")
(replace-match "DESCRIPTION:ical test task 1 modified"))
(message "PUTting changed description")
(should (org-caldav-save-resource
(concat (org-caldav-events-url) (url-hexify-string "orgcaldavtest@cal3.ics"))
(encode-coding-string (buffer-string) 'utf-8))))
(message "5th SYNC")
(let ((org-caldav-sync-changes-to-org 'all))
(org-caldav-sync))
(should (equal `((,org-caldav-calendar-id "orgcaldavtest@cal3"
changed-in-cal cal->org))
org-caldav-sync-result))
(with-current-buffer (find-file-noselect org-caldav-test-inbox)
(goto-char (point-min))
(should (re-search-forward
"* TODO Changed A test task from iCal
\\s-*SCHEDULED: <2012-12-24 Mon>
\\s-*:PROPERTIES:
\\s-*:ID:\\s-*orgcaldavtest@cal3
\\s-*:END:
\\s-*ical test task 1 modified")))
(message "Deleting event in Org")
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(goto-char (point-min))
(message (buffer-string))
(should (search-forward
"* DONE [#B] Changed Another test task from Org :test:
CLOSED: [2012-12-24 Mon 00:00] DEADLINE: <2012-12-23 Sun> SCHEDULED: <2012-12-19 Wed>
:PROPERTIES:
:ID: orgcaldavtest-org5
:END:
Org task 2
"))
;; Delete this event in Org
(replace-match ""))
;; Sync
(message "6th SYNC")
(org-caldav-sync)
;; Event should be deleted in calendar
(let ((calevents (org-caldav-get-event-etag-list)))
(should (= (length calevents) 3))
(should-not (assoc '"orgcaldavtest-org5" calevents)))
(should
(equal org-caldav-sync-result
`((,org-caldav-calendar-id "orgcaldavtest-org5" deleted-in-org removed-from-cal))))
(should-not
(assoc '"orgcaldavtest-org5" org-caldav-event-list))
;; Delete event in calendar
(message "Delete event in calendar")
(should (org-caldav-delete-event "orgcaldavtest@org4"))
;; Sync one last time
(message "7th SYNC")
(let ((org-caldav-delete-org-entries 'always))
(org-caldav-sync))
(should
(equal org-caldav-sync-result
`((,org-caldav-calendar-id "orgcaldavtest@org4" deleted-in-cal removed-from-org))))
;; There shouldn't be anything left in that buffer
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(goto-char (point-min))
(should-not (re-search-forward "[:alnum:]" nil t)))
(should-not
(assoc '"orgcaldavtest@org4" org-caldav-event-list))
(org-caldav-test-cleanup)))
(ert-deftest org-caldav-10-test-description-cleanup ()
(let ((org-caldav-sync-todo t)
(org-icalendar-include-todo 'all))
(message "Setting up temporary files")
(org-caldav-test-setup-temp-files)
(setq org-caldav-calendar-id (car org-caldav-test-calendar-names))
;; Set up data for org-caldav.
(setq org-caldav-files (list org-caldav-test-orgfile))
(setq org-caldav-inbox org-caldav-test-inbox)
(setq org-caldav-debug-level 2)
(message "Cleaning up upstream calendars")
(org-caldav-test-set-up)
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(insert "* TODO Test links preserved in description
:PROPERTIES:
:ID: org-caldav-10-test-links-preserved
:END:
https://orgmode.org
* Test timestamp is filtered from description
:PROPERTIES:
:ID: org-caldav-10-test-timestamp-filtered
:END:
<2023-01-06 Fri>
* 2nd test for timestamp filtering
:PROPERTIES:
:ID: org-caldav-10-test-timestamp-filtered2
:END:
<2023-01-06 Fri 14:00>--<2023-01-06 Fri 15:00>")
(save-buffer))
(org-caldav-sync)
(with-current-buffer (org-caldav-get-event
"org-caldav-10-test-links-preserved")
(goto-char (point-min))
(save-excursion
(should (search-forward "DESCRIPTION:"))))
(with-current-buffer (org-caldav-get-event
"org-caldav-10-test-timestamp-filtered")
(goto-char (point-min))
(save-excursion
(should (not (re-search-forward
"DESCRIPTION:.*2023-01-06" nil t)))))
(with-current-buffer (org-caldav-get-event
"org-caldav-10-test-timestamp-filtered2")
(goto-char (point-min))
(save-excursion
(should (not (re-search-forward
"DESCRIPTION:.*2023-01-06" nil t))))))
(org-caldav-test-cleanup))
(ert-deftest org-caldav-11-test-sync-unsaved ()
(org-caldav-test-setup-temp-files)
(setq org-caldav-files (list org-caldav-test-orgfile))
(setq org-caldav-inbox org-caldav-test-inbox)
(setq org-caldav-debug-level 2)
(setq org-caldav-calendar-id (car org-caldav-test-calendar-names))
(org-caldav-test-set-up)
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
;; Make sure the file exists
(save-buffer)
;; Add an event, but don't save it
(insert org-caldav-test-org2))
(org-caldav-sync))
(ert-deftest org-caldav-12-test-doublesync-created-id ()
(org-caldav-test-setup-temp-files)
(setq org-caldav-files (list org-caldav-test-orgfile))
(setq org-caldav-inbox org-caldav-test-inbox)
(setq org-caldav-debug-level 2)
(setq org-caldav-calendar-id (car org-caldav-test-calendar-names))
(org-caldav-test-set-up)
(with-current-buffer (find-file-noselect org-caldav-test-orgfile)
(insert "* This is a test without ID\n"
" <2009-08-08 Sat 10:00>\n whatever\n foo\n bar\n")
(save-buffer))
;; First sync creates an ID for the event
(org-caldav-sync)
;; Test if second sync can find the ID we created. If not, the test
;; will exit with org-caldav error "Could not find UID"
(org-caldav-sync))
org-caldav-master/org-caldav.el 0000664 0000000 0000000 00000271665 14504053514 0016751 0 ustar 00root root 0000000 0000000 ;;; org-caldav.el --- Sync org files with external calendar through CalDAV
;; Copyright (C) 2012-2017 Free Software Foundation, Inc.
;; Copyright (C) 2018-2023 David Engster
;; Author: David Engster
;; Contributor: Jack Kamm
;; Keywords: calendar, caldav
;; URL: https://github.com/dengste/org-caldav/
;; Package-Requires: ((emacs "26.3") (org "9.1"))
;;
;; Version: 3.0
;;
;; This file is not part of GNU Emacs.
;;
;; org-caldav.el 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.
;; org-caldav.el 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 GNU Emacs. If not, see .
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;; Commentary:
;; See README.
;;; Code:
(require 'url-dav)
(require 'url-http) ;; b/c of Emacs bug
(require 'ox-icalendar)
(require 'org-id)
(require 'icalendar)
(require 'url-util)
(require 'cl-lib)
(require 'button)
(declare-function oauth2-url-retrieve-synchronously "ext:oauth2" (&rest args))
(declare-function oauth2-auth-and-store "ext:oauth2" (&rest args))
(defgroup org-caldav nil
"Sync org files with external calendar through CalDAV."
:prefix "org-caldav-"
:group 'calendar)
(defcustom org-caldav-url "https://my.calendarserver.invalid/caldav"
"Base URL for CalDAV access.
By default, `org-caldav-calendar-id' will be appended to this to
get an URL for calendar events. If this default is not correct,
use '%s' to where the calendar id must be placed.
Set this to symbol \\='google to use Google Calendar, using OAuth2
authentication. In that case, also make sure that
`browse-url-browser-function' is set to a Javascript-capable
browser (like `browse-url-firefox'). Note that you need to have
the OAuth2 library installed, and you will also have to set
`org-caldav-oauth2-client-id' and
`org-caldav-oauth2-client-secret' (see README).
In general, if this variable is a symbol, do OAuth2
authentication with access URIs set in
`org-caldav-oauth2-providers'."
:type 'string)
(defcustom org-caldav-calendar-id "mycalendar"
"ID of your calendar."
:type 'string)
(defcustom org-caldav-uuid-extension ".ics"
"The file extension to add to uuids in webdav requests.
This is usually .ics, but on some servers (davmail), it is .EML"
:type 'string)
(defcustom org-caldav-files '("~/org/appointments.org")
"List of files which should end up in calendar.
The file in `org-caldav-inbox' is implicitly included, so you
don't have to add it here."
:type '(repeat string))
(defcustom org-caldav-select-tags nil
"List of tags to filter the synced tasks.
If any such tag is found in a buffer, all items that do not carry
one of these tags will not be exported."
:type '(repeat string))
(defcustom org-caldav-exclude-tags nil
"List of tags to exclude from the synced tasks.
All items that carry one of these tags will not be exported."
:type '(repeat string))
(defcustom org-caldav-inbox "~/org/appointments.org"
"Where to put new entries obtained from calendar.
This can be simply be a filename to an Org file where all new
entries will be put. It can also be a list, in which case you
can choose between the following options (which are a subset of
the allowed targets in `org-capture-templates'):
- (file \"path/to/file\"), or
- (id \"id of existing org entry\"), or
- (file+headline \"path/to/file\" \"node headline\"), or
- (file+olp \"path/to/file\" \"Level 1 headline\" \"Level 2\" ...), or
- (file+olp+datetree \"path/to/file\" \"Level 1 heading\" ...)
For datetree, use `org-caldav-datetree-treetype' to control the
tree-type; see its Help for more info about the datetree behavior."
:type 'file)
(defcustom org-caldav-datetree-treetype 'month
"Tree-type when `org-caldav-inbox' is a datetree.
This can be one of `month', `week', or `day'.
Entries are filed into the datetree according to their start
date. TODO's without a scheduled date instead use the current
date at sync time.
Currently, this only controls where new entries are filed;
existing entries whose dates change won't be refiled
automatically.
Since existing entries aren't refiled automatically yet, the
datetree is more useful for light organization of the org-file,
rather than as a precise reflection of your calendar. Therefore
the default value is at the coarsest level of month-tree."
:type '(choice
(const month :tag "Month-tree")
(const week :tag "Week-Day-tree")
(const day :tag "Month-Day-tree")))
(defcustom org-caldav-sync-direction 'twoway
"Which kind of sync should be done between Org and calendar.
Default is `twoway', meaning that changes in Org are synced to the
calendar and changes in the calendar are synced back to
Org. Other options are:
`org->cal': Only sync Org to calendar
`cal->org': Only sync calendar to Org"
:type '(choice
(const twoway :tag "Two-way sync")
(const org->cal :tag "Only sync org to calendar")
(const cal->org :tag "Only sync calendar to Org")))
(defcustom org-caldav-calendars nil
"A list of plists which define different calendars.
Use this variable to sync with several different remote
calendars. By setting this, `org-caldav-sync' will run several
times, and you can set the global variables like
`org-caldav-calendar-id' for each run through plist keys.
The available keys are: :url, :calendar-id, :files, :select-tags,
:inbox, :skip-conditions, :sync-inbox, and :sync-direction. They
override the corresponding global org-caldav-* variables. You can
also use any other key, which will then override any org-*
variable.
Example:
\\='((:calendar-id \"work@whatever\" :files (\"~/org/work.org\")
:inbox \"~/org/fromwork.org\")
(:calendar-id \"stuff@mystuff\"
:files (\"~/org/sports.org\" \"~/org/play.org\")
:inbox \"~/org/fromstuff.org\"))"
:type '(repeat (plist)))
(defcustom org-caldav-save-directory user-emacs-directory
"Directory where org-caldav saves its sync state."
:type 'directory)
(defcustom org-caldav-sync-changes-to-org 'title-and-timestamp
"What kind of changes should be synced from Calendar to Org.
Can be one of the following symbols:
title-and-timestamp: Sync title and timestamp (default).
title-only: Sync only the title.
timestamp-only: Sync only the timestamp.
all: Sync everything.
When choosing `all', you should be aware of the fact that the
iCalendar format is pretty limited in what it can store, so you
might loose information in your Org items (take a look at
`org-icalendar-include-body')."
:type '(choice
(const title-and-timestamp :tag "Sync title and timestamp")
(const title-only :tag "Sync only the title")
(const timestamp-only :tag "Sync only the timestamp")
(const all :tag "Sync everything")))
(defcustom org-caldav-days-in-past nil
"Number of days before today to skip in the exported calendar.
This makes it very easy to keep the remote calendar clean.
nil means include all entries (default)
any number set will cut the dates older than N days in the past."
:type 'integer)
(defcustom org-caldav-delete-org-entries 'ask
"Whether entries deleted in calendar may be deleted in Org.
Can be one of the following symbols:
ask = Ask for before deletion (default)
never = Never delete Org entries
always = Always delete"
:type '(choice
(const ask :tag "Ask before deletion")
(const never :tag "Never delete Org entries")
(const always :tag "Always delete")))
(defcustom org-caldav-delete-calendar-entries 'ask
"Whether entries deleted in Org may be deleted in calendar.
Can be one of the following symbols:
always = Always delete without asking (default)
ask = Ask for before deletion
never = Never delete calendar entries"
:type '(choice
(const ask :tag "Ask before deletion")
(const never :tag "Never delete calendar entries")
(const always :tag "Always delete without asking")))
(defcustom org-caldav-skip-conditions nil
"Conditions for skipping entries during icalendar export.
This must be a list of conditions, which are described in the
doc-string of `org-agenda-skip-if'. Any entry that matches will
not be exported. Note that the normal `org-agenda-skip-function'
has no effect on the icalendar exporter."
:type 'list)
(defcustom org-caldav-backup-file
(expand-file-name "org-caldav-backup.org" user-emacs-directory)
"Name of the file where org-caldav should backup entries.
Set this to nil if you don't want any backups.
Note that the ID property of the backup entry is renamed to
OLDID, to prevent org-id-find from returning the backup entry in
future syncs."
:type 'file)
(defcustom org-caldav-show-sync-results 'with-headings
"Whether to show what was done after syncing.
If this is the symbol `with-headings', the results will also
include headings from Org entries."
:type '(choice
(const with-headings :tag "Show what was done after syncing including headings")
(const nil :tag "Don't show what was done after syncing")))
(defcustom org-caldav-retry-attempts 5
"Number of times trying to retrieve/put events."
:type 'integer)
(defcustom org-caldav-calendar-preamble
"BEGIN:VCALENDAR\nPRODID:-//emacs//org-caldav//EN\nVERSION:2.0\nCALSCALE:GREGORIAN\n"
"Preamble used for iCalendar events.
You usually should not have to touch this, but it might be
necessary to add timezone information here in case your CalDAV
server does not do that for you, or if you want to use a
different timezone in your Org files."
:type 'string)
(defcustom org-caldav-sync-todo nil
"Whether to sync TODO's with the CalDav server. If you enable
this, you should also set `org-icalendar-include-todo' to
`all'.
This feature is relatively new and less well tested; it is
recommended to have backups before enabling it."
:type 'boolean)
(defcustom org-caldav-todo-priority '((0 nil) (1 "A") (5 "B") (9 "C"))
"Mapping between iCalendar and Org TODO priority levels.
The iCalendar priority is an integer 1-9, with lower number
having higher priority, and 0 equal to unspecified priority. The
default Org priorities are A-C, but this can be changed with
`org-priority-highest' and `org-priority-lowest'. If you change
the default Org priority, you should also update this
variable (`org-caldav-todo-priority').
The default mapping is: 0 is no priority, 1-4 is #A, 5-8 is #B,
and 9 is #C.
TODO: Store the priority in a property and sync it."
:type 'list)
(defcustom org-caldav-todo-percent-states '((0 "TODO") (100 "DONE"))
"Mapping between `org-todo-keywords' & iCal VTODO's percent-complete.
iCalendar's percent-complete is a positive integer between 0 and
100. The default value for `org-caldav-todo-percent-states' maps
these to `org-todo-keywords' as follows: 0-99 is TODO, and 100 is
DONE.
The following example would instead map 0 to TODO, 1 to NEXT,
2-99 to PROG, and 100 to DONE:
(setq org-caldav-todo-percent-states
\\='((0 \"TODO\") (1 \"NEXT\") (2 \"PROG\") (100 \"DONE\")))
Note: You should check that the keywords in
`org-caldav-todo-percent-states' are also valid keywords in
`org-todo-keywords'."
:type 'list)
(defcustom org-caldav-todo-deadline-schedule-warning-days nil
"Whether to auto-create SCHEDULED timestamp from DEADLINE.
When set to `t', on sync any TODO item with a DEADLINE timestamp
will have a SCHEDULED timestamp added if it doesn't already have
one.
This uses the warning string like DEADLINE: <2017-07-05 Wed -3d>
to a SCHEDULED <2017-07-02 Sun>. If the warning days (here -3d)
is not given it is taken from `org-deadline-warning-days'.
This might be useful for OpenTasks users, to prevent the app from
showing tasks which have a deadline years in the future."
:type 'boolean)
(defcustom org-caldav-debug-level 1
"Level of debug output in `org-caldav-debug-buffer'.
0 or nil: no debug output. 1: Normal debugging. 2: Excessive
debugging (this will also output event content into the
buffer)."
:type 'integer)
(defcustom org-caldav-debug-buffer "*org-caldav-debug*"
"Name of the debug buffer."
:type 'string)
(defcustom org-caldav-resume-aborted 'ask
"Whether aborted sync attempts should be resumed.
Can be one of the following symbols:
ask = Ask for before resuming (default)
never = Never resume
always = Always resume"
:type '(choice
(const ask :tag "Ask before resuming")
(const never :tag "Never resume")
(const always :tag "Always resume")))
(defcustom org-caldav-oauth2-providers
'((google "https://accounts.google.com/o/oauth2/v2/auth"
"https://www.googleapis.com/oauth2/v4/token"
"https://www.googleapis.com/auth/calendar"
"https://apidata.googleusercontent.com/caldav/v2/%s/events"))
"List of providers that need OAuth2. Each must be of the form
IDENTIFIER AUTH-URL TOKEN-URL RESOURCE-URL CALENDAR-URL
where IDENTIFIER is a symbol that can be set in `org-caldav-url'
and '%s' in the CALENDAR-URL denotes where
`org-caldav-calendar-id' must be placed to generate a valid
events URL for a calendar."
:type 'list)
(defcustom org-caldav-oauth2-client-id nil
"Client ID for OAuth2 authentication."
:type 'string)
(defcustom org-caldav-oauth2-client-secret nil
"Client secret for OAuth2 authentication."
:type 'string)
(defcustom org-caldav-location-newline-replacement ", "
"String to replace newlines in the LOCATION field with."
:type 'string)
(defcustom org-caldav-save-buffers t
"Whether to save Org buffers modified by sync.
Note this might be needed for some versions of Org (9.5+?), which
have trouble finding IDs in unsaved buffers, causing syncs and
the unit tests to fail otherwise."
:type 'boolean)
(defcustom org-caldav-description-blank-line-before t
"Whether DESCRIPTION inserted into org should be preceded by blank line."
:type 'boolean)
(defcustom org-caldav-description-blank-line-after t
"Whether DESCRIPTION inserted into org should be followed by blank line."
:type 'boolean)
;; Internal variables
(defvar org-caldav-oauth2-available
(condition-case nil (require 'oauth2) (error))
"Whether oauth2 library is available.")
(defvar org-caldav-previous-calendar nil
"The plist from org-caldav-calendars, which holds the last
synced calendar. Used to properly resume an interupted attempt.")
(defvar org-caldav-event-list nil
"The event list database.
This is an alist with elements
(uid md5 etag sequence status).
It will be saved to disk between sessions.")
(defvar org-caldav-sync-result nil
"Result from last synchronization.
Contains an alist with entries
(calendar-id uid status action)
with status = {new,changed,deleted}-in-{org,cal}
and action = {org->cal, cal->org, error:org->cal, error:cal->org}.")
(defvar org-caldav-empty-calendar nil
"Flag if we have an empty calendar in the beginning.")
(defvar org-caldav-ics-buffer nil
"Buffer holding the ICS data.")
(defvar org-caldav-oauth2-tokens nil
"Tokens for OAuth2 authentication.")
(defvar org-caldav-previous-files nil
"Files that were synced during previous run.")
(defmacro org-caldav--suppress-obsolete-warning (var body)
"Macro for compatibility.
To be removed when emacs dependency reaches >=27.1."
(declare (indent defun))
(if (fboundp 'with-suppressed-warnings)
`(with-suppressed-warnings ((obsolete ,var))
,body)
`(with-no-warnings ,body)))
(defsubst org-caldav-add-event (uid md5 etag sequence status)
"Add event with UID, MD5, ETAG and STATUS."
(setq org-caldav-event-list
(append org-caldav-event-list
(list (list uid md5 etag sequence status)))))
(defsubst org-caldav-search-event (uid)
"Return entry with UID from even list."
(assoc uid org-caldav-event-list))
(defsubst org-caldav-event-md5 (event)
"Get MD5 from EVENT."
(nth 1 event))
(defsubst org-caldav-event-etag (event)
"Get etag from EVENT."
(nth 2 event))
(defsubst org-caldav-event-sequence (event)
"Get sequence number from EVENT."
(nth 3 event))
(defsubst org-caldav-event-status (event)
"Get status from EVENT."
(nth 4 event))
(defsubst org-caldav-event-set-status (event status)
"Set status from EVENT to STATUS."
(setcar (last event) status))
(defsubst org-caldav-event-set-etag (event etag)
"Set etag from EVENT to ETAG."
(setcar (nthcdr 2 event) etag))
(defsubst org-caldav-event-set-md5 (event md5sum)
"Set md5 from EVENT to MD5SUM."
(setcar (cdr event) md5sum))
(defsubst org-caldav-event-set-sequence (event seqnum)
"Set sequence number from EVENT to SEQNUM."
(setcar (nthcdr 3 event) seqnum))
(defsubst org-caldav-use-oauth2 ()
(symbolp org-caldav-url))
(defun org-caldav-filter-events (status)
"Return list of events with STATUS."
(delq nil
(mapcar
(lambda (event)
(when (eq (car (last event)) status)
event))
org-caldav-event-list)))
;; Since not being able to access an URL via DAV is the most reported
;; error, let's be very verbose about checking for DAV availability.
(defun org-caldav-check-dav (url)
"Check if URL accepts DAV requests.
Report an error with further details if that is not the case."
(let* ((buffer (org-caldav-url-retrieve-synchronously url "OPTIONS"))
(header nil)
(options nil))
(when (not buffer)
(error "Retrieving URL %s failed." url))
(with-current-buffer buffer
(when (zerop (buffer-size))
(error "Not data received for URL %s (maybe TLS problem)." url))
(goto-char (point-min))
(when (not (re-search-forward "^HTTP[^ ]* \\([0-9]+ .*\\)$"
(line-end-position) t))
(switch-to-buffer buffer)
(error "No valid HTTP response from URL %s." url))
(let ((response (match-string 1)))
(when (not (string-match "2[0-9][0-9].*" response))
(switch-to-buffer buffer)
(error "Error while checking for OPTIONS at URL %s: %s" url response)))
(mail-narrow-to-head)
(let ((davheader (mail-fetch-field "dav")))
(when (not davheader)
(switch-to-buffer buffer)
(error "The URL %s does not accept DAV requests" url)))))
t)
(defun org-caldav-check-oauth2 (provider)
"Check if we have to do OAuth2 authentication.
If that is the case, also check that everything is installed and
configured correctly, and throw an user error otherwise."
(when (null (assoc provider org-caldav-oauth2-providers))
(user-error (concat "No OAuth2 provider found for %s in "
"`org-caldav-oauth2-providers'")
(symbol-name provider)))
(when (not org-caldav-oauth2-available)
(user-error (concat "Oauth2 library is missing "
"(install from GNU ELPA)")))
(when (or (null org-caldav-oauth2-client-id)
(null org-caldav-oauth2-client-secret))
(user-error (concat "You need to set oauth2 client ID and secret "
"for OAuth2 authentication"))))
(defun org-caldav-retrieve-oauth2-token (provider calendar-id)
"Do OAuth2 authentication for PROVIDER with CALENDAR-ID."
(let ((cached-token
(assoc
(concat (symbol-name provider) "__" calendar-id)
org-caldav-oauth2-tokens)))
(if cached-token
(cdr cached-token)
(let* ((ids (assoc provider org-caldav-oauth2-providers))
(token (oauth2-auth-and-store (nth 1 ids) (nth 2 ids) (nth 3 ids)
org-caldav-oauth2-client-id
org-caldav-oauth2-client-secret)))
(when (null token)
(user-error "OAuth2 authentication failed"))
(setq org-caldav-oauth2-tokens
(append org-caldav-oauth2-tokens
(list (cons (concat (symbol-name provider) "__" calendar-id)
token))))
token))))
(defun org-caldav-url-retrieve-synchronously (url &optional
request-method
request-data
extra-headers)
"Retrieve URL with REQUEST-METHOD, REQUEST-DATA and EXTRA-HEADERS.
This will switch to OAuth2 if necessary."
(if (org-caldav-use-oauth2)
(oauth2-url-retrieve-synchronously
(org-caldav-retrieve-oauth2-token org-caldav-url org-caldav-calendar-id)
url request-method request-data
extra-headers)
(let ((url-request-method request-method)
(url-request-data request-data)
(url-request-extra-headers extra-headers))
(url-retrieve-synchronously url))))
(defun org-caldav-namespace-bug-workaround (buffer)
"Workaraound for Emacs bug #23440 on Emacs version <26.
This is needed for the Radicale CalDAV server which uses DAV as
default namespace."
(when (< emacs-major-version 26)
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(when (re-search-forward "<[^>]* \\(xmlns=\"DAV:\"\\)" nil t)
(replace-match "xmlns:DAV=\"DAV:\"" nil nil nil 1)
(goto-char (match-beginning 0))
(while (re-search-forward "?" nil t)
(insert "DAV:")))))))
(defun org-caldav-url-dav-get-properties (url property)
"Retrieve PROPERTY from URL.
Output is the same as `url-dav-get-properties'. This switches to
OAuth2 if necessary."
(let ((request-data (concat "\n"
"\n"
"\n"))
(extra '(("Depth" . "1") ("Content-type" . "text/xml"))))
(let ((resultbuf (org-caldav-url-retrieve-synchronously
url "PROPFIND" request-data extra))
(retr 1))
(while (and (= 0 (buffer-size resultbuf)) (< retr org-caldav-retry-attempts))
(org-caldav-debug-print 1 (format "org-caldav-url-dav-get-properties: could not get data from url: %s\n trying again..." url))
(setq resultbuf (org-caldav-url-retrieve-synchronously
url "PROPFIND" request-data extra))
(setq retr (1+ retr)))
;; Check if we got a valid result for PROPFIND
(with-current-buffer resultbuf
(goto-char (point-min))
(when (not (re-search-forward "^HTTP[^ ]* \\([0-9]+ .*\\)$"
(line-end-position) t))
(switch-to-buffer resultbuf)
(error "No valid HTTP response from URL %s." url))
(let ((response (match-string 1)))
(when (not (string-match "2[0-9][0-9].*" response))
(switch-to-buffer resultbuf)
(error "Error while doing PROPFIND for '%s' at URL %s: %s" property url response))))
(org-caldav-namespace-bug-workaround resultbuf)
(url-dav-process-response resultbuf url))))
(defun org-caldav-check-connection ()
"Check connection by doing a PROPFIND on CalDAV URL.
Also sets `org-caldav-empty-calendar' if calendar is empty."
(org-caldav-debug-print 1 (format "Check connection for %s."
(org-caldav-events-url)))
(org-caldav-check-dav (org-caldav-events-url))
(let* ((output (org-caldav-url-dav-get-properties
(org-caldav-events-url) "resourcetype"))
(status (plist-get (cdar output) 'DAV:status)))
;; We accept any 2xx status. Since some CalDAV servers return 404
;; for a newly created and not yet used calendar, we accept it as
;; well.
(unless (or (= (/ status 100) 2)
(= status 404))
(org-caldav-debug-print 1 "Got error status from PROPFIND: " output)
(error "Could not query CalDAV URL %s." (org-caldav-events-url)))
(if (= status 404)
(progn
(org-caldav-debug-print 1 "Got 404 status - assuming calendar is new and empty.")
(setq org-caldav-empty-calendar t))
(when (= (length output) 1)
;; This is an empty calendar; fetching etags might return 404.
(org-caldav-debug-print 1 "This is an empty calendar. Setting flag.")
(setq org-caldav-empty-calendar t)))
t))
;; This defun is partly taken out of url-dav.el, written by Bill Perry.
(defun org-caldav-get-icsfiles-etags-from-properties (properties)
"Return all ics files and etags from PROPERTIES."
(let (prop files)
(while (setq prop (pop properties))
(let ((url (car prop))
(etag (plist-get (cdr prop) 'DAV:getetag)))
(if (string-match (concat ".*/\\(.+\\)\\" org-caldav-uuid-extension "/?$") url)
(setq url (match-string 1 url))
(setq url nil))
(when (string-match "\"\\(.*\\)\"" etag)
(setq etag (match-string 1 etag)))
(when (and url etag)
(push (cons (url-unhex-string url) etag) files))))
files))
(defun org-caldav-get-event-etag-list ()
"Return list of events with associated etag from remote calendar.
Return list with elements (uid . etag)."
(if org-caldav-empty-calendar
nil
(let ((output (org-caldav-url-dav-get-properties
(org-caldav-events-url) "getetag")))
(cond
((> (length output) 1)
;; Everything looks OK - we got a list of "things".
;; Get all ics files and etags you can find in there.
(org-caldav-get-icsfiles-etags-from-properties output))
((or (null output)
(zerop (length output)))
;; This is definitely an error.
(error "Error while getting eventlist from %s." (org-caldav-events-url)))
((and (= (length output) 1)
(stringp (car-safe (car output))))
(let ((status (plist-get (cdar output) 'DAV:status)))
(if (eq status 200)
;; This is an empty directory
'empty
(if status
(error "Error while getting eventlist from %s. Got status code: %d."
(org-caldav-events-url) status)
(error "Error while getting eventlist from %s."
(org-caldav-events-url))))))))))
(defun org-caldav-get-event (uid &optional with-headers)
"Get event with UID from calendar.
Function returns a buffer containing the event, or nil if there's
no such event.
If WITH-HEADERS is non-nil, do not delete headers.
If retrieve fails, do `org-caldav-retry-attempts' retries."
(org-caldav-debug-print 1 (format "Getting event UID %s." uid))
(let ((counter 0)
eventbuffer errormessage)
(while (and (not eventbuffer)
(< counter org-caldav-retry-attempts))
(with-current-buffer
(org-caldav-url-retrieve-synchronously
(concat (org-caldav-events-url) (url-hexify-string uid) org-caldav-uuid-extension))
(goto-char (point-min))
(if (looking-at "HTTP.*2[0-9][0-9]")
(setq eventbuffer (current-buffer))
;; There was an error retrieving the event
(setq errormessage (buffer-substring (point-min) (line-end-position)))
(setq counter (1+ counter))
(org-caldav-debug-print
1 (format "(Try %d) Error when trying to retrieve UID %s: %s"
counter uid errormessage)))))
(unless eventbuffer
;; Give up
(error "Failed to retrieve UID %s after %d tries with error %s"
uid org-caldav-retry-attempts errormessage))
(with-current-buffer eventbuffer
(unless (search-forward "BEGIN:VCALENDAR" nil t)
(error "Failed to find calendar entry for UID %s (see buffer %s)"
uid (buffer-name eventbuffer)))
(beginning-of-line)
(unless with-headers
(delete-region (point-min) (point)))
(save-excursion
(while (re-search-forward "\^M" nil t)
(replace-match "")))
;; Join lines because of bug in icalendar parsing.
(save-excursion
(while (re-search-forward "^ " nil t)
(delete-char -2)))
(org-caldav-debug-print 2 (format "Content of event UID %s: " uid)
(buffer-string)))
eventbuffer))
(defun org-caldav-convert-buffer-to-crlf ()
"Converts local buffer to the dos format using crlf at the end
of the line. Some ical validators fail otherwise."
(save-excursion
(goto-char (point-min))
(while (not (= (point) (point-max)))
(goto-char (- (line-end-position) 1))
(unless (string= (thing-at-point 'char) "\^M")
(forward-char)
(insert "\^M"))
(forward-line))))
(defun org-caldav-put-event (buffer)
"Add event in BUFFER to calendar.
The filename will be derived from the UID."
(let ((event (with-current-buffer buffer (buffer-string))))
(with-temp-buffer
(insert org-caldav-calendar-preamble event "END:VCALENDAR\n")
(goto-char (point-min))
(let* ((uid (org-caldav-get-uid))
(url (concat (org-caldav-events-url) (url-hexify-string uid) org-caldav-uuid-extension)))
(org-caldav-debug-print 1 (format "Putting event UID %s." uid))
(org-caldav-debug-print 2 (format "Content of event UID %s: " uid)
(buffer-string))
(setq org-caldav-empty-calendar nil)
(org-caldav-save-resource
(concat (org-caldav-events-url) uid org-caldav-uuid-extension)
(encode-coding-string (buffer-string) 'utf-8))))))
(defun org-caldav-url-dav-delete-file (url)
"Delete URL.
Will switch to OAuth2 if necessary."
(org-caldav-url-retrieve-synchronously url "DELETE"))
(defun org-caldav-delete-event (uid)
"Delete event UID from calendar.
Returns t on success and nil if an error occurs. The error will
be caught and a message displayed instead."
(org-caldav-debug-print 1 (format "Deleting event UID %s." uid))
(condition-case err
(progn
(org-caldav-url-dav-delete-file
(concat (org-caldav-events-url) uid org-caldav-uuid-extension))
t)
(error
(progn
(message "Could not delete URI %s." uid)
(org-caldav-debug-print 1 "Got error while removing UID:" err)
nil))))
(defun org-caldav-delete-everything (prefix)
"Delete all events from Calendar and removes state file.
Again: This deletes all events in your calendar. So only do this
if you're really sure. This has to be called with a prefix, just
so you don't do it by accident."
(interactive "P")
(if (not prefix)
(message "This function has to be called with a prefix.")
(unless (or org-caldav-empty-calendar
(not (y-or-n-p "This will delete EVERYTHING in your calendar. \
Are you really sure? ")))
(let ((events (org-caldav-get-event-etag-list))
(counter 0)
(url-show-status nil))
(dolist (cur events)
(setq counter (1+ counter))
(message "Deleting event %d of %d" counter (length events))
(org-caldav-delete-event (car cur)))
(setq org-caldav-empty-calendar t))
(when (file-exists-p
(org-caldav-sync-state-filename org-caldav-calendar-id))
(delete-file (org-caldav-sync-state-filename org-caldav-calendar-id)))
(setq org-caldav-event-list nil)
(setq org-caldav-sync-result nil)
(message "Done"))))
(defun org-caldav-events-url ()
"Return URL for events."
(let* ((url
(if (org-caldav-use-oauth2)
(nth 4 (assoc org-caldav-url org-caldav-oauth2-providers))
org-caldav-url))
(eventsurl
(if (string-match ".*%s.*" url)
(format url org-caldav-calendar-id)
(concat url "/" org-caldav-calendar-id "/"))))
(if (string-match ".*/$" eventsurl)
eventsurl
(concat eventsurl "/"))))
(defun org-caldav-update-eventdb-from-org (buf)
"With combined ics file in BUF, update the event database."
(org-caldav-debug-print 1 "=== Updating EventDB from Org")
(with-current-buffer buf
(goto-char (point-min))
(while (org-caldav-narrow-next-event)
(let* ((uid (org-caldav-rewrite-uid-in-event))
(md5 (unless (string-match "^orgsexp-" uid)
(org-caldav-generate-md5-for-org-entry uid)))
(event (org-caldav-search-event uid)))
(cond
((null event)
;; Event does not yet exist in DB, so add it.
(org-caldav-debug-print 1
(format "Org UID %s: New" uid))
(org-caldav-add-event uid md5 nil nil 'new-in-org))
((not (string= md5 (org-caldav-event-md5 event)))
;; Event exists but has changed MD5, so mark it as changed.
(org-caldav-debug-print 1
(format "Org UID %s: Changed" uid))
(org-caldav-event-set-md5 event md5)
(org-caldav-event-set-status event 'changed-in-org))
((eq (org-caldav-event-status event) 'new-in-org)
;; FIXME This only detects duplicate events that are new. It
;; would also be nice to detect duplicate events that
;; changed (#267). But this is complicated to detect b/c
;; status 'changed-in-org could be from the previous sync
(org-caldav-debug-print 1
(format "Org UID %s: Error. Double entry." uid))
(push (list org-caldav-calendar-id
uid 'new-in-org 'error:double-entry)
org-caldav-sync-result))
(t
(org-caldav-debug-print 1
(format "Org UID %s: Synced" uid))
(org-caldav-event-set-status event 'in-org)))))
;; Mark events deleted in Org
(dolist (cur (org-caldav-filter-events nil))
(org-caldav-debug-print
1 (format "Cal UID %s: Deleted in Org" (car cur)))
(org-caldav-event-set-status cur 'deleted-in-org))))
(defun org-caldav-update-eventdb-from-cal ()
"Update event database from calendar."
(org-caldav-debug-print 1 "=== Updating EventDB from Cal")
(let ((events (org-caldav-get-event-etag-list))
dbentry)
(dolist (cur events)
;; Search entry in database.
(setq dbentry (org-caldav-search-event (car cur)))
(cond
((not dbentry)
;; Event is not yet in database, so add it.
(org-caldav-debug-print 1
(format "Cal UID %s: New" (car cur)))
(org-caldav-add-event (car cur) nil (cdr cur) nil 'new-in-cal))
((eq (org-caldav-event-status dbentry) 'ignored)
(org-caldav-debug-print 1 (format "Cal UID %s: Ignored." (car cur))))
((or (eq (org-caldav-event-status dbentry) 'changed-in-org)
(eq (org-caldav-event-status dbentry) 'deleted-in-org))
(org-caldav-debug-print 1
(format "Cal UID %s: Ignoring (Org always wins)." (car cur))))
((null (org-caldav-event-etag dbentry))
(org-caldav-debug-print 1
(format "Cal UID %s: No Etag. Mark as change, so putting it again." (car cur)))
(org-caldav-event-set-status dbentry 'changed-in-org))
((not (string= (cdr cur) (org-caldav-event-etag dbentry)))
;; Event's etag changed.
(org-caldav-debug-print 1
(format "Cal UID %s: Changed" (car cur)))
(org-caldav-event-set-status dbentry 'changed-in-cal)
(org-caldav-event-set-etag dbentry (cdr cur)))
((null (org-caldav-event-status dbentry))
;; Event was deleted in Org
(org-caldav-debug-print 1
(format "Cal UID %s: Deleted in Org" (car cur)))
(org-caldav-event-set-status dbentry 'deleted-in-org))
((eq (org-caldav-event-status dbentry) 'in-org)
(org-caldav-debug-print 1
(format "Cal UID %s: Synced" (car cur)))
(org-caldav-event-set-status dbentry 'synced))
((eq (org-caldav-event-status dbentry) 'changed-in-org)
;; Do nothing
)
(t
(error "Unknown status; this is probably a bug."))))
;; Mark events deleted in cal.
(dolist (cur (org-caldav-filter-events 'in-org))
(org-caldav-debug-print 1
(format "Cal UID %s: Deleted in Cal" (car cur)))
(org-caldav-event-set-status cur 'deleted-in-cal))))
(defun org-caldav-generate-md5-for-org-entry (uid)
"Find Org entry with UID and calculate its MD5."
(let ((marker (org-id-find uid t)))
(when (null marker)
(error "Could not find UID %s." uid))
(with-current-buffer (marker-buffer marker)
(goto-char (marker-position marker))
(md5 (buffer-substring-no-properties
(org-entry-beginning-position)
(org-entry-end-position))))))
(defun org-caldav-var-for-key (key)
"Return associated global org-caldav variable for key KEY."
(cl-case key
(:url 'org-caldav-url)
(:calendar-id 'org-caldav-calendar-id)
(:files 'org-caldav-files)
(:select-tags 'org-caldav-select-tags)
(:exclude-tags 'org-caldav-exclude-tags)
(:inbox 'org-caldav-inbox)
(:skip-conditions 'org-caldav-skip-conditions)
(:sync-direction 'org-caldav-sync-direction)
(t (intern
(concat "org-"
(substring (symbol-name key) 1))))))
(defsubst org-caldav-sync-do-cal->org ()
"True if we have to sync from calendar to org."
(member org-caldav-sync-direction '(twoway cal->org)))
(defsubst org-caldav-sync-do-org->cal ()
"True if we have to sync from org to calendar."
(member org-caldav-sync-direction '(twoway org->cal)))
(defun org-caldav-get-org-files-for-sync ()
"Return list of all org files for syncing.
This adds the inbox if necessary."
(let ((inbox (org-caldav-inbox-file org-caldav-inbox)))
(append org-caldav-files
(when (and inbox
(org-caldav-sync-do-org->cal)
(not (member inbox org-caldav-files)))
(list inbox)))))
(defun org-caldav-sync-calendar (&optional calendar resume)
"Sync one calendar, optionally provided through plist CALENDAR.
The format of CALENDAR is described in `org-caldav-calendars'.
If CALENDAR is not provided, the default values will be used.
If RESUME is non-nil, try to resume."
(setq org-caldav-empty-calendar nil)
(setq org-caldav-previous-calendar calendar)
(let (calkeys calvalues oauth-enable)
;; Extrace keys and values from 'calendar' for progv binding.
(dolist (i (number-sequence 0 (1- (length calendar)) 2))
(setq calkeys (append calkeys (list (nth i calendar)))
calvalues (append calvalues (list (nth (1+ i) calendar)))))
(cl-progv (mapcar 'org-caldav-var-for-key calkeys) calvalues
(when (org-caldav-sync-do-org->cal)
(let ((files-for-sync (org-caldav-get-org-files-for-sync)))
(dolist (filename files-for-sync)
(when (not (file-exists-p filename))
(if (yes-or-no-p (format "File %s does not exist, create it?" filename))
(write-region "" nil filename)
(user-error "File %s does not exist" filename))))
;; prevent https://github.com/dengste/org-caldav/issues/230
(org-id-update-id-locations files-for-sync)))
;; Check if we need to do OAuth2
(when (org-caldav-use-oauth2)
;; We need to do oauth2. Check if it is available.
(org-caldav-check-oauth2 org-caldav-url)
;; Retrieve token
(org-caldav-retrieve-oauth2-token org-caldav-url org-caldav-calendar-id))
(let ((numretry 0)
success)
(while (null success)
(condition-case err
(progn
(org-caldav-check-connection)
(setq success t))
(error
(if (= numretry (1- org-caldav-retry-attempts))
(org-caldav-check-connection)
(org-caldav-debug-print
1 "Got error while checking connection (will try again):" err)
(cl-incf numretry))))))
(unless resume
(setq org-caldav-event-list nil
org-caldav-previous-files nil)
(org-caldav-load-sync-state)
;; Check if org files were removed.
(when org-caldav-previous-files
(let ((missing (cl-set-difference org-caldav-previous-files
org-caldav-files
:test #'string=)))
(when (and missing
(not (yes-or-no-p
(concat "WARNING: Previously synced file(s) are missing: "
(mapconcat 'identity missing ",")
"%s. Are you sure you want to sync? "))))
(user-error "Sync aborted"))))
;; Remove status in event list
(dolist (cur org-caldav-event-list)
(unless (eq (org-caldav-event-status cur) 'ignored)
(org-caldav-event-set-status cur nil)))
;; Update events for the org->cal direction
(when (org-caldav-sync-do-org->cal)
;; Export Org to icalendar format
(setq org-caldav-ics-buffer (org-caldav-generate-ics))
(org-caldav-update-eventdb-from-org org-caldav-ics-buffer))
;; Update events for the cal->org direction
(when (org-caldav-sync-do-cal->org)
(org-caldav-update-eventdb-from-cal)))
(when (org-caldav-sync-do-org->cal)
(org-caldav-update-events-in-cal org-caldav-ics-buffer))
(when (org-caldav-sync-do-cal->org)
(org-caldav-update-events-in-org))
(org-caldav-save-sync-state)
(setq org-caldav-event-list nil)
(when (org-caldav-sync-do-org->cal)
(with-current-buffer org-caldav-ics-buffer
(set-buffer-modified-p nil)
(kill-buffer))
(delete-file (buffer-file-name org-caldav-ics-buffer))))))
;;;###autoload
(defun org-caldav-sync ()
"Sync Org with calendar."
(interactive)
(unless (or (> emacs-major-version 24)
(and (= emacs-major-version 24)
(> emacs-minor-version 2)))
(user-error "You have to use at least Emacs 24.3"))
(org-caldav-debug-print 1 "========== Started sync.")
(if (and org-caldav-event-list
(not (eq org-caldav-resume-aborted 'never))
(or (eq org-caldav-resume-aborted 'always)
(and (eq org-caldav-resume-aborted 'ask)
(y-or-n-p "Last sync seems to have been aborted. \
Should I try to resume? "))))
(org-caldav-sync-calendar org-caldav-previous-calendar t)
(setq org-caldav-sync-result nil)
(if (null org-caldav-calendars)
(org-caldav-sync-calendar)
(dolist (calendar org-caldav-calendars)
(org-caldav-debug-print 1 "Syncing first calendar entry:" calendar)
(org-caldav-sync-calendar calendar))))
(when org-caldav-show-sync-results
(org-caldav-display-sync-results))
(message "Finished sync."))
(defun org-caldav-update-events-in-cal (icsbuf)
"Update events in calendar.
ICSBUF is the buffer containing the exported iCalendar file."
(org-caldav-debug-print 1 "=== Updating events in calendar")
(with-current-buffer icsbuf
(widen)
(goto-char (point-min))
(let ((events (append (org-caldav-filter-events 'new-in-org)
(org-caldav-filter-events 'changed-in-org)))
(counter 0)
(url-show-status nil)
(event-etag (org-caldav-get-event-etag-list))
uid)
;; Put the events via CalDAV.
(dolist (cur events)
(setq counter (1+ counter))
(if (eq (org-caldav-event-etag cur) 'put)
(org-caldav-debug-print 1
(format "Event UID %s: Was already put previously." (car cur)))
(org-caldav-debug-print 1
(format "Event UID %s: Org --> Cal" (car cur)))
(widen)
(goto-char (point-min))
(while (and (setq uid (org-caldav-get-uid))
(not (string-match (car cur) uid))))
(unless (string-match (car cur) uid)
(error "Could not find UID %s" (car cur)))
(org-caldav-narrow-event-under-point)
(org-caldav-convert-buffer-to-crlf)
(org-caldav-cleanup-ics-description)
(org-caldav-maybe-fix-timezone)
(org-caldav-set-sequence-number cur event-etag)
(org-caldav-fix-todo-priority)
(org-caldav-fix-todo-status-percent-state)
(org-caldav-fix-categories)
(org-caldav-fix-todo-dtstart)
(message "Putting event %d of %d Org --> Cal" counter (length events))
(if (org-caldav-put-event icsbuf)
(org-caldav-event-set-etag cur 'put)
(org-caldav-debug-print 1
(format "Event UID %s: Error while doing Org --> Cal" (car cur)))
(org-caldav-event-set-status cur 'error)
(push (list org-caldav-calendar-id (car cur)
'error 'error:org->cal)
org-caldav-sync-result))))
;; Get Etags
(setq event-etag (org-caldav-get-event-etag-list))
(dolist (cur events)
(let ((etag (assoc (car cur) event-etag)))
(when (and (not (eq (org-caldav-event-status cur) 'error))
etag)
(org-caldav-event-set-etag cur (cdr etag))
(push (list org-caldav-calendar-id (car cur)
(org-caldav-event-status cur) 'org->cal)
org-caldav-sync-result)))))
;; Remove events that were deleted in org
(unless (eq org-caldav-delete-calendar-entries 'never)
(let ((events (org-caldav-filter-events 'deleted-in-org))
(url-show-status nil)
(counter 0))
(dolist (cur events)
(setq counter (1+ counter))
(when (or (eq org-caldav-delete-calendar-entries 'always)
(y-or-n-p (format "Delete event '%s' from external calendar?"
(org-caldav-get-calendar-summary-from-uid
(car cur)))))
(message "Deleting event %d from %d" counter (length events))
(org-caldav-delete-event (car cur))
(push (list org-caldav-calendar-id (car cur)
'deleted-in-org 'removed-from-cal)
org-caldav-sync-result)
(setq org-caldav-event-list
(delete cur org-caldav-event-list))))))
;; Remove events that could not be put
(dolist (cur (org-caldav-filter-events 'error))
(setq org-caldav-event-list
(delete cur org-caldav-event-list)))))
(defun org-caldav-set-sequence-number (event event-etag)
"Set sequence number in ics and in eventdb for EVENT.
EVENT-ETAG is the current list of events and etags on the server.
The ics must be in the current buffer."
(save-excursion
(let ((seq (org-caldav-event-sequence event))
retrieve)
(unless (or seq
(not (assoc (car event) event-etag)))
;; We don't have a sequence yet, but event is already in the
;; calendar, hence we have to get the current number first.
(setq retrieve (org-caldav-get-event (car event)))
(when (null retrieve)
;; Retrieving the event failed... so let's just use '1' and
;; hope it works.
(org-caldav-debug-print 1 (format "UID %s: Failed to retrieve item from server." (car event)))
(org-caldav-debug-print 1 (format "UID %s: Use sequence number 1 and hope for the best." (car event)))
(setq seq 0)) ;; incremented below
(unless seq
(with-current-buffer (org-caldav-get-event (car event))
(goto-char (point-min))
(if (re-search-forward "^SEQUENCE:\\s-*\\([0-9]+\\)" nil t)
(progn
(setq seq (string-to-number (match-string 1)))
(org-caldav-debug-print 1 (format "UID %s: Got sequence number %d" (car event) seq)))
(org-caldav-debug-print 1 (format "UID %s: Event does not have sequence number, start with 1." (car event)))
(setq seq 0))))) ;; incremented below
(when seq
(setq seq (1+ seq))
(goto-char (point-min))
(if (re-search-forward "^SEQUENCE:" nil t)
(delete-region (line-beginning-position) (+ 1 (line-end-position)))
(goto-char (point-min))
(re-search-forward "^SUMMARY:")
(forward-line))
(beginning-of-line)
(insert "SEQUENCE:" (number-to-string seq) "\n")
(org-caldav-event-set-sequence event seq)))))
(defun org-caldav-cleanup-ics-description ()
"Cleanup description for event in current buffer.
This removes an initial timestamp or range if it wasn't removed
by ox-icalendar."
(save-excursion
(goto-char (point-min))
(when (re-search-forward
;; Can't use org-tsr-regexp because -- is converted to
;; unicode emdash –
(concat "^DESCRIPTION:\\(\\s-*"
org-ts-regexp
"\\(–"
org-ts-regexp
"\\)?\\(\\\\n\\\\n\\)?\\)")
nil t)
(replace-match "" nil nil nil 1))))
(defun org-caldav-maybe-fix-timezone ()
"Fix the timezone if it is all uppercase.
This is a bug in older Org versions."
(unless (null org-icalendar-timezone)
(save-excursion
(goto-char (point-min))
(while (search-forward (upcase org-icalendar-timezone) nil t)
(replace-match org-icalendar-timezone t)))))
(defun org-caldav-fix-todo-priority ()
"icalendar exports default priority with ical export. We want
a priority of 0 if is not set."
(save-excursion
(goto-char (point-min))
(when (search-forward "BEGIN:VTODO" nil t)
(search-forward "PRIORITY:")
(unless (eq (thing-at-point 'number) 0)
;; NOTE: Deletion up to eol-1 assumes the line ends with ^M
(delete-region (point) (- (line-end-position) 1))
(insert (number-to-string
(save-excursion
(goto-char (point-min))
(org-id-goto (org-caldav-get-uid))
(org-narrow-to-subtree)
(let ((nprio (if (re-search-forward org-priority-regexp nil t)
(let* ((prio (org-entry-get nil "PRIORITY"))
(r 0))
(dolist (pri org-caldav-todo-priority r)
(when (string= (car (cdr pri)) prio)
(setq r (car pri))))
r)
0)))
(widen)
nprio))))))))
(defun org-caldav-fix-todo-status-percent-state ()
"icalendar exports only sets the STATUS but not the
PERCENT-COMPLETE. This works great if you have only TODO and
DONE, but I like to use other states like STARTED or NEXT to
indicate the process. This fixes the ical values for that.
TODO: save percent-complete also as a property in org"
(save-excursion
(goto-char (point-min))
(when (search-forward "BEGIN:VTODO" nil t)
(if (search-forward "STATUS:" nil t)
(delete-region (line-beginning-position) (+ 1 (line-end-position)))
(progn (search-forward "END:VTODO")
(goto-char (line-beginning-position))))
(let* ((state (save-excursion
(goto-char (point-min))
(org-id-goto (org-caldav-get-uid))
(substring-no-properties (org-get-todo-state))))
(r nil)
(percent (dolist (p org-caldav-todo-percent-states r)
(when (string= state (car (cdr p)))
(setq r (car p)))))
(status (if r
(cond ((= percent 0) "NEEDS-ACTION")
((= percent 100) "COMPLETED")
(t "IN-PROCESS"))
(error "Error setting percent state: '%s' not present in org-caldav-todo-percent-states" state)))
(completed (save-excursion
(goto-char (point-min))
(org-id-goto (org-caldav-get-uid))
(org-element-property :closed (org-element-at-point)))))
(insert "PERCENT-COMPLETE:" (number-to-string percent) "\n")
(insert "STATUS:" status "\n")
;; if closed missing but in DONE state:
(when (and (= percent 100) (not completed))
(setq completed (save-excursion
(goto-char (point-min))
(org-id-goto (org-caldav-get-uid))
(org-add-planning-info 'closed (org-current-effective-time))
(org-element-property :closed (org-element-at-point)))))
(when completed
(insert (org-icalendar-convert-timestamp
completed "COMPLETED") "\n"))))))
(defun org-caldav-fix-categories ()
"Nextcloud creates an empty category if this is set without any
entry. We fix this by removing the CATEGORIES entry."
(save-excursion
(goto-char (point-min))
(when (and (search-forward "CATEGORIES:" nil t)
(not (thing-at-point 'word)))
(delete-region (line-beginning-position) (+ (line-end-position) 1)))))
(defun org-caldav-fix-todo-dtstart ()
"ox-icalendar includes the actual time as DTSTART into the
vtodo. For nextcloud this behaviour is undesired, because
dtstart is used for the beginning of the task, which is in the
SCHEDULED of the org entry. Lets see if the org entry has a
scheduled time and remove dtstart if it doesn't.
If `org-caldav-todo-deadline-schedule-warning-days' is set, this will
also look if there is a deadline."
(save-excursion
(goto-char (point-min))
(when (search-forward "BEGIN:VTODO" nil t)
(when
(search-forward "DTSTART" nil t)
(unless
(save-excursion
(goto-char (point-min))
(org-id-goto (org-caldav-get-uid))
(if (and org-caldav-todo-deadline-schedule-warning-days
;; has deadline warning days set too:
(string-match "-\\([0-9]+\\)\\([hdwmy]\\)\\(\\'\\|>\\| \\)"
(or (org-entry-get nil "DEADLINE" nil) "")))
(or (org-get-scheduled-time nil) (org-get-deadline-time nil))
(org-get-scheduled-time nil)))
(delete-region (line-beginning-position) (+ 1 (line-end-position))))))))
(defun org-caldav-inbox-file (inbox)
"Return file name associated with INBOX.
For format of INBOX, see `org-caldav-inbox'."
(cond ((stringp inbox)
inbox)
((memq (car inbox) '(file file+headline file+olp file+olp+datetree))
(nth 1 inbox))
((eq (car inbox) 'id)
(org-id-find-id-file (nth 1 inbox)))))
(defun org-caldav-inbox-point-and-level (inbox eventdata-alist)
"Return position and level where to add new entries in INBOX.
For format of INBOX, see `org-caldav-inbox'. The values are
returned as a cons (POINT . LEVEL)."
(save-excursion
(cond ((or (stringp inbox) (eq (car inbox) 'file))
(cons (point-max) 1))
((eq (car inbox) 'file+headline)
(let ((org-link-search-inhibit-query t))
(org-link-search (concat "*" (nth 2 inbox)) nil t)
(org-caldav--point-level-helper)))
((eq (car inbox) 'file+olp)
(goto-char (org-find-olp (cdr inbox)))
(org-caldav--point-level-helper))
((eq (car inbox) 'file+olp+datetree)
(let ((outline-path (cdr (cdr inbox))))
(when outline-path
(goto-char (org-find-olp (cdr inbox))))
(funcall
(pcase org-caldav-datetree-treetype
(`week #'org-datetree-find-iso-week-create)
(`month #'org-caldav--datetree-find-month-create)
(`day #'org-datetree-find-date-create))
(let ((start-d (alist-get 'start-d eventdata-alist)))
(if start-d (org-caldav--convert-to-calendar start-d)
;; Use current date for a VTODO without DTSTART
(calendar-current-date)))
(when outline-path 'subtree-at-point))
(org-caldav--point-level-helper)))
((eq (car inbox) 'id)
(goto-char (cdr (org-id-find (nth 1 inbox))))
(org-caldav--point-level-helper)))))
(defun org-caldav--datetree-find-month-create (d keep-restriction)
"Helper function for compatibility.
To be removed when emacs dependency reaches >=27.2."
;; NOTE: package-lint may give an erroneous warning here. See:
;; https://github.com/purcell/package-lint/issues/240
(if (fboundp 'org-datetree-find-month-create)
(org-datetree-find-month-create d keep-restriction)
(error "Need to update to Org 9.4 to use monthtree.")))
(defun org-caldav--point-level-helper ()
"Helper function for `org-caldav-inbox-point-and-level'.
Go to the end of the current subtree, and return the point and
level to add a new child entry."
(let ((level (1+ (org-current-level))))
(org-end-of-subtree t t)
(cons (point) level)))
(defun org-caldav--todo-percent-to-state (npercent)
"Converts percentage to keyword in `org-caldav-todo-percent-states'."
(let (tstate)
(dolist (to org-caldav-todo-percent-states
tstate)
(when (>= npercent (car to))
(setq tstate (car (cdr to)))))))
(defun org-caldav-update-events-in-org ()
"Update events in Org files."
(org-caldav-debug-print 1 "=== Updating events in Org")
(let ((events (append (org-caldav-filter-events 'new-in-cal)
(org-caldav-filter-events 'changed-in-cal)))
(url-show-status nil)
(counter 0)
eventdata-alist buf uid timesync is-todo)
(dolist (cur events)
(catch 'next
(setq uid (car cur))
(setq counter (1+ counter))
(message "Getting event %d of %d" counter (length events))
(with-current-buffer (org-caldav-get-event uid)
;; Get sequence number
(goto-char (point-min))
(setq is-todo (when (save-excursion (re-search-forward
"^BEGIN:VTODO$" nil t))
t))
(when (and is-todo (not org-caldav-sync-todo))
(message "Skipping TODO entry.")
(org-caldav-event-set-status cur 'ignored)
(throw 'next nil))
(save-excursion
(when (re-search-forward "^SEQUENCE:\\s-*\\([0-9]+\\)" nil t)
(org-caldav-event-set-sequence
cur (string-to-number (match-string 1)))))
(setq eventdata-alist (org-caldav-convert-event-or-todo is-todo)))
(cond
((eq (org-caldav-event-status cur) 'new-in-cal)
;; This is a new event.
(condition-case nil
(with-current-buffer (find-file-noselect
(org-caldav-inbox-file org-caldav-inbox))
(let ((point-and-level (org-caldav-inbox-point-and-level
org-caldav-inbox eventdata-alist)))
(org-caldav-debug-print
1 (format "Event UID %s: New in Cal --> Org inbox." uid))
(goto-char (car point-and-level))
(org-caldav-insert-org-event-or-todo
(append eventdata-alist
`((uid . ,uid)
(level . ,(cdr point-and-level))))))
(push (list org-caldav-calendar-id uid
(org-caldav-event-status cur) 'cal->org)
org-caldav-sync-result)
(setq buf (current-buffer))
(when org-caldav-save-buffers (save-buffer)))
(error
;; inbox file/headline could not be found
(org-caldav-event-set-status cur 'error)
(push (list org-caldav-calendar-id uid
(org-caldav-event-status cur) 'error:inbox-notfound)
org-caldav-sync-result)
(throw 'next nil))))
((string-match "^orgsexp-" uid)
;; This was generated by a org-sexp, we cannot sync it this way.
(org-caldav-debug-print
1 (format "Event UID %s: Changed in Cal, but this is a sexp entry \
which can only be synced to calendar. Ignoring." uid))
(org-caldav-event-set-status cur 'synced)
(push (list org-caldav-calendar-id uid
(org-caldav-event-status cur) 'error:changed-orgsexp)
org-caldav-sync-result)
(throw 'next nil))
(t
;; This is a changed event.
(org-caldav-debug-print
1 (format "Event UID %s: Changed in Cal --> Org" uid))
(let-alist (append eventdata-alist
`((marker . ,(org-id-find (car cur) t))))
(when (null .marker)
(error "Could not find UID %s." (car cur)))
(with-current-buffer (marker-buffer .marker)
(goto-char (marker-position .marker))
(when org-caldav-backup-file
(org-caldav-backup-item))
;; See what we should sync.
(when (or (eq org-caldav-sync-changes-to-org 'title-only)
(eq org-caldav-sync-changes-to-org 'title-and-timestamp))
;; Sync title
(org-caldav-change-heading .summary)
(if (not is-todo)
;; Sync location
(org-caldav-change-location .location)
;; Sync priority
(let* ((nprio (string-to-number (or .priority "0")))
(r nil)
(vprio (dolist (p org-caldav-todo-priority r)
(when (>= nprio (car p))
(setq r (car (cdr p)))))))
(when vprio
(org-priority (string-to-char vprio))))
;; Sync todo status
(org-todo (org-caldav--todo-percent-to-state
(string-to-number .percent-complete)))
;; Sync categories
(org-caldav-set-org-tags .categories)))
(when (or (eq org-caldav-sync-changes-to-org 'timestamp-only)
(eq org-caldav-sync-changes-to-org 'title-and-timestamp))
(if (not is-todo)
;; Sync timestamp; also sets timesync to 'orgsexp
;; if unable to sync due to s-expression
(progn
(org-narrow-to-subtree)
(goto-char (point-min))
(setq timesync
(if (search-forward "<%%(" nil t)
'orgsexp
;; org-caldav-create-time-range can mess
;; with replace-match, so we let-bind tr
;; before calling re-search-forward
(let ((tr (org-caldav-create-time-range
.start-d .start-t
.end-d .end-t .end-type)))
(when (re-search-forward org-tsr-regexp nil t)
(replace-match tr nil t)))))
(widen))
;; Sync scheduled
(when .start-d
(org--deadline-or-schedule
nil 'scheduled (org-caldav-convert-to-org-time
.start-d .start-t)))
;; Sync deadline
(when .due-d
(org--deadline-or-schedule
nil 'deadline (org-caldav-convert-to-org-time
.due-d .due-t)))
;; Sync completion time
(when .completed-d (org-add-planning-info
'closed (org-caldav-convert-to-org-time
.completed-d .completed-t)))))
(when (eq org-caldav-sync-changes-to-org 'all)
;; Sync everything, so first remove the old one.
(let ((level (org-current-level)))
(delete-region (org-entry-beginning-position)
(org-entry-end-position))
(org-caldav-insert-org-event-or-todo
(append eventdata-alist `((uid . ,uid)
(level . ,level))))))
(setq buf (current-buffer))
(push (list org-caldav-calendar-id uid
(org-caldav-event-status cur)
(if (eq timesync 'orgsexp)
'error:changed-orgsexp 'cal->org))
org-caldav-sync-result)
(when org-caldav-save-buffers (save-buffer))))))
;; Update the event database.
(org-caldav-event-set-status cur 'synced)
(with-current-buffer buf
(org-caldav-event-set-md5
cur (md5 (buffer-substring-no-properties
(org-entry-beginning-position)
(org-entry-end-position))))))))
;; (Maybe) delete entries which were deleted in calendar.
(unless (eq org-caldav-delete-org-entries 'never)
(dolist (cur (org-caldav-filter-events 'deleted-in-cal))
(org-id-goto (car cur))
(when (or (eq org-caldav-delete-org-entries 'always)
(and (eq org-caldav-delete-org-entries 'ask)
(y-or-n-p "Delete this entry locally? ")))
(delete-region (org-entry-beginning-position)
(org-entry-end-position))
(when org-caldav-save-buffers (save-buffer))
(setq org-caldav-event-list
(delete cur org-caldav-event-list))
(org-caldav-debug-print 1
(format "Event UID %s: Deleted from Org" (car cur)))
(push (list org-caldav-calendar-id (car cur)
'deleted-in-cal 'removed-from-org)
org-caldav-sync-result)))))
(defun org-caldav--org-show-subtree ()
"Helper function for compatibility.
To be removed when org dependency reaches >=9.6."
(if (fboundp 'org-fold-show-subtree)
(org-fold-show-subtree)
(org-caldav--suppress-obsolete-warning org-show-subtree
(org-show-subtree))))
(defun org-caldav-change-heading (newheading)
"Change heading from Org item under point to NEWHEADING."
(org-narrow-to-subtree)
(goto-char (point-min))
(org-caldav--org-show-subtree)
(when (and (re-search-forward org-complex-heading-regexp nil t)
(match-string 4))
(let ((start (match-beginning 4))
(end (match-end 4)))
;; Check if a timestring is in the heading
(goto-char start)
(save-excursion
(when (re-search-forward org-ts-regexp-both end t)
;; Check if timestring is at the beginning or end of heading
(if (< (- end (match-end 0))
(- (match-beginning 0) start))
(setq end (1- (match-beginning 0)))
(setq start (1+ (match-end 0))))))
(delete-region start end)
(goto-char start)
(insert newheading)))
(widen))
(defun org-caldav-change-location (newlocation)
"Change the LOCATION property from ORG item under point to
NEWLOCATION. If NEWLOCATION is \"\", removes the location property. If
NEWLOCATION contains newlines, replace them with
`org-caldav-location-newline-replacement'."
(let ((replacement org-caldav-location-newline-replacement))
(cl-assert (not (string-match-p "\n" replacement)))
(if (> (length newlocation) 0)
(org-set-property "LOCATION"
(replace-regexp-in-string "\n" replacement newlocation))
(org-delete-property "LOCATION"))))
(defun org-caldav-backup-item ()
"Put current item in backup file."
(let ((item (buffer-substring (org-entry-beginning-position)
(org-entry-end-position))))
(with-temp-buffer
(org-mode)
(insert item "\n")
;; Rename the ID property to OLDID, to prevent org-id-find from
;; returning the backup entry in future syncs
(goto-char (point-min))
(let* ((entry (org-element-at-point))
(uid (org-element-property :ID entry)))
(when uid
(org-set-property "OLDID" uid)
(org-delete-property "ID")))
(write-region (point-min) (point-max) org-caldav-backup-file t))))
(defun org-caldav-skip-function (backend)
(org-caldav-debug-print 2 "Skipping over excluded entries")
(when (eq backend 'icalendar)
(org-map-entries
(lambda ()
(let ((pt (save-excursion (apply 'org-agenda-skip-entry-if org-caldav-skip-conditions)))
(ts (when org-caldav-days-in-past (* (abs org-caldav-days-in-past) -1)))
(stamp (or (org-entry-get nil "TIMESTAMP" t) (org-entry-get nil "CLOSED" t))))
(when (or pt (and stamp ts (> ts (org-time-stamp-to-now stamp))))
(delete-region (point) (org-end-of-subtree t t))
(setq org-map-continue-from (point)))))))
(org-caldav-debug-print 2 "Finished skipping"))
(defun org-caldav-timestamp-has-time-p (timestamp)
"Checks whether a timestamp has a time.
Returns nil if not and (sec min hour) if it has."
(let ((ti (parse-time-string timestamp)))
(or (nth 0 ti) (nth 1 ti) (nth 2 ti))))
(defun org-caldav-prepare-scheduled-deadline-timestamps (orgfiles)
"For nextcloud (or maybe the ical standard?) in vtodo the
scheduled and deadline have all have a time specified or none of
them. So we find todo items which have deadline and scheduled
specified, but one of them has and the other do not have any
time, and we ask the user to fix that."
(org-map-entries
(lambda ()
(let ((sched (org-entry-get nil "SCHEDULED"))
(deadl (org-entry-get nil "DEADLINE"))
kchoice)
(when (and sched deadl)
(when (and (org-caldav-timestamp-has-time-p sched)
(not (org-caldav-timestamp-has-time-p deadl)))
(org-id-goto (org-id-get-create))
(setq kchoice (read-char-choice "Scheduled and Deadline
set. For syncing you need to (s) set time on
DEADLINE, or (d) delete SCHEDULED time."
'(?s ?d)))
(cond ((= kchoice ?s) (org-deadline nil))
((= kchoice ?d) (org--deadline-or-schedule nil 'scheduled
(replace-regexp-in-string " [0-2][0-9]:[0-5][0-9]" "" sched)))))
(when (and (not (org-caldav-timestamp-has-time-p sched))
(org-caldav-timestamp-has-time-p deadl))
(org-id-goto (org-entry-get nil "ID"))
(setq kchoice (read-char-choice "Scheduled and Deadline
set. For syncing you need to (s) set time on
SCHEDULED, or (d) delete DEADLINE time."
'(?s ?d)))
(cond ((= kchoice ?s) (org-schedule nil))
((= kchoice ?d) (org--deadline-or-schedule nil 'deadline
(replace-regexp-in-string " [0-2][0-9]:[0-5][0-9]" "" sched))))))))
nil orgfiles))
(defun org-caldav-create-uid (file &optional bell)
"Set ID property on headlines missing it in FILE.
When optional argument BELL is non-nil, inform the user with
a message if the file was modified. This func is the same as
org-icalendar-create-uid except that it ignores entries that
match org-caldav-skip-conditions."
(let (modified-flag)
(org-map-entries
(lambda ()
(let ((entry (org-element-at-point)))
(unless (org-element-property :ID entry)
(unless (apply 'org-agenda-skip-entry-if org-caldav-skip-conditions)
(org-id-get-create)
(setq modified-flag t)
(forward-line)))))
nil nil 'comment)
(when (and bell modified-flag)
(message "ID properties created in file \"%s\"" file)
(sit-for 2))))
(defun org-caldav-generate-ics ()
"Generate ICS file from `org-caldav-files'.
Returns buffer containing the ICS file."
(let ((icalendar-file
(if (featurep 'ox-icalendar)
'org-icalendar-combined-agenda-file
'org-combined-agenda-icalendar-file))
(orgfiles (org-caldav-get-org-files-for-sync))
(org-export-select-tags org-caldav-select-tags)
(org-icalendar-exclude-tags org-caldav-exclude-tags)
;; We create UIDs ourselves and do not rely on ox-icalendar.el
(org-icalendar-store-UID nil)
;; Does not work yet
(org-icalendar-include-bbdb-anniversaries nil)
(icalendar-uid-format "orgsexp-%h")
(org-icalendar-date-time-format
(cond
((and org-icalendar-timezone
(string= org-icalendar-timezone "UTC"))
":%Y%m%dT%H%M%SZ")
(org-icalendar-timezone
";TZID=%Z:%Y%m%dT%H%M%S")
(t
":%Y%m%dT%H%M%S"))))
(dolist (orgfile orgfiles)
(with-current-buffer (org-get-agenda-file-buffer orgfile)
(org-caldav-debug-print
2 (format "Checking %s for new entries & unsaved changes" orgfile))
(org-caldav-create-uid orgfile t)
(when (and org-caldav-save-buffers
(buffer-modified-p))
(org-caldav-debug-print 2 (format "Saving %s" orgfile))
(save-buffer))))
;; check scheduled and deadline for having both time or none (vtodo)
(org-caldav-prepare-scheduled-deadline-timestamps orgfiles)
(set icalendar-file (make-temp-file "org-caldav-"))
(org-caldav-debug-print 1 (format "Generating ICS file %s."
(symbol-value icalendar-file)))
;; compat: use org-export-before-parsing-functions after org >=9.6
(org-caldav--suppress-obsolete-warning org-export-before-parsing-hook
(let ((org-export-before-parsing-hook
(append org-export-before-parsing-hook
(when (or org-caldav-skip-conditions
org-caldav-days-in-past)
'(org-caldav-skip-function))
(when org-caldav-todo-deadline-schedule-warning-days
'(org-caldav-scheduled-from-deadline)))))
;; Export events to one single ICS file.
(apply 'org-icalendar--combine-files orgfiles)))
(find-file-noselect (symbol-value icalendar-file))))
(defun org-caldav-get-uid ()
"Get UID for event in current buffer."
(if (re-search-forward "^UID:\\s-*\\(.+\\)\\s-*$" nil t)
(let ((case-fold-search nil)
(uid (match-string 1)))
(while (progn (forward-line)
(looking-at " \\(.+\\)\\s-*$"))
(setq uid (concat uid (match-string 1))))
(while (string-match "\\s-+" uid)
(setq uid (replace-match "" nil nil uid)))
(when (string-match "^\\(\\(DL\\|SC\\|TS\\|TODO\\)[0-9]*-\\)" uid)
(setq uid (replace-match "" nil nil uid)))
uid)
(error "No UID could be found for current event.")))
(defun org-caldav-narrow-next-event ()
"Narrow next event in the current buffer.
If buffer is currently not narrowed, narrow to the first one.
Returns nil if there are no more events."
(if (not (org-caldav-buffer-narrowed-p))
(goto-char (point-min))
(goto-char (point-max))
(widen))
(if (null (re-search-forward "BEGIN:V[EVENT|TODO]" nil t))
(progn
;; No more events.
(widen) nil)
(beginning-of-line)
(narrow-to-region (point)
(save-excursion
(re-search-forward "END:V[EVENT|TODO]")
(forward-line 1)
(point)))
t))
(defun org-caldav-narrow-event-under-point ()
"Narrow ics event in the current buffer under point."
(unless (or (looking-at "BEGIN:VEVENT") (looking-at "BEGIN:VTODO"))
(when (null (re-search-backward "BEGIN:V[EVENT|TODO]" nil t))
(error "Cannot find event under point."))
(beginning-of-line))
(narrow-to-region (point)
(save-excursion
(re-search-forward "END:V[EVENT|TODO]")
(forward-line 1)
(point))))
(defun org-caldav-rewrite-uid-in-event ()
"Rewrite UID in current buffer.
This will strip prefixes like 'DL' or 'TS' the Org exporter puts
in the UID and also remove whitespaces. Throws an error if there
is no UID to rewrite. Returns the UID."
(save-excursion
(goto-char (point-min))
(let ((uid (org-caldav-get-uid)))
(when uid
(goto-char (point-min))
(re-search-forward "^UID:")
(let ((pos (point)))
(while (progn (forward-line)
(looking-at " \\(.+\\)\\s-*$")))
(delete-region pos (point)))
(insert uid "\n"))
uid)))
(defun org-caldav-debug-print (level &rest objects)
"Print OBJECTS into debug buffer with debug level LEVEL.
Do nothing if LEVEL is larger than `org-caldav-debug-level'."
(unless (or (null org-caldav-debug-level)
(> level org-caldav-debug-level))
(with-current-buffer (get-buffer-create org-caldav-debug-buffer)
(dolist (cur objects)
(if (stringp cur)
(insert cur)
(prin1 cur (current-buffer)))
(insert "\n")))))
(defun org-caldav-buffer-narrowed-p ()
"Return non-nil if current buffer is narrowed."
(> (buffer-size) (- (point-max)
(point-min))))
(defun org-caldav-insert-org-event-or-todo (eventdata-alist)
"Insert org block from given event data at current position.
Elements of EVENTDATA-ALIST are passed on as arguments to
`org-caldav-insert-org-entry' and `org-caldav-insert-org-todo'.
Returns MD5 from entry."
(let-alist eventdata-alist
(if (eq .component-type 'todo)
(org-caldav-insert-org-todo
.start-d .start-t .due-d .due-t .priority .percent-complete
.summary .description .completed-d .completed-t .categories
.uid .level)
(org-caldav-insert-org-entry
.start-d .start-t .end-d .end-t .summary
.description .location .e-type .uid .level))))
(defun org-caldav--insert-description (description)
(when (> (length description) 0)
(when org-caldav-description-blank-line-before (newline))
(let ((beg (point)))
(insert description)
(org-indent-region beg (point)))
(when org-caldav-description-blank-line-after (newline))
(newline)))
(defun org-caldav-insert-org-entry (start-d start-t end-d end-t
summary description location e-type
&optional uid level)
"Insert org block from given data at current position.
START/END-D: Start/End date. START/END-T: Start/End time.
SUMMARY, DESCRIPTION, LOCATION, UID: obvious.
Dates must be given in a format `org-read-date' can parse.
If LOCATION is \"\", no LOCATION: property is written.
If UID is nil, no UID: property is written.
If LEVEL is nil, it defaults to 1.
Returns MD5 from entry."
(insert (make-string (or level 1) ?*) " " summary "\n")
(insert (if org-adapt-indentation " " "")
(org-caldav-create-time-range start-d start-t end-d end-t e-type) "\n")
(org-caldav--insert-description description)
(forward-line -1)
(when uid
(org-set-property "ID" (url-unhex-string uid)))
(org-caldav-change-location location)
(org-caldav-insert-org-entry--wrapup))
(defun org-caldav-insert-org-todo (start-d start-t due-d due-t
priority percent-complete
summary description
completed-d completed-t
categories
&optional uid level)
"Insert org block from given data at current position.
START/DUE-D: Start/Due date. START/DUE-T: Start/Due time.
PRIORITY: 0-9, PERCENT-COMPLETE: 0-100.
See `org-caldav-todo-priority' and
`org-caldav-todo-percent-states' for explanations how this values
are used.
SUMMARY, DESCRIPTION, UID: obvious.
Dates must be given in a format `org-read-date' can parse.
If UID is nil, no UID: property is written.
If LEVEL is nil, it defaults to 1.
Returns MD5 from entry."
(let* ((nprio (string-to-number (or priority "0")))
(r nil)
(vprio (dolist (p org-caldav-todo-priority r)
(when (>= nprio (car p))
(setq r (car (cdr p))))))
(prio (if vprio (concat "[#" vprio "] ") "")))
(insert (make-string (or level 1) ?*) " "
(org-caldav--todo-percent-to-state
(string-to-number (or percent-complete "0")))
" " prio summary "\n"))
(org-caldav--insert-description description)
(forward-line -1)
(when start-d
(org--deadline-or-schedule
nil 'scheduled (org-caldav-convert-to-org-time start-d start-t)))
(when due-d
(org--deadline-or-schedule
nil 'deadline (org-caldav-convert-to-org-time due-d due-t)))
(when completed-d
(org-add-planning-info 'closed (org-caldav-convert-to-org-time completed-d completed-t)))
(org-caldav-set-org-tags categories)
(when uid (org-set-property "ID" (url-unhex-string uid)))
(org-caldav-insert-org-entry--wrapup))
(defun org-caldav--org-set-tags-to (tags)
"Helper function for compatibility.
To be removed when org dependency reaches >=9.2."
(org-caldav--suppress-obsolete-warning org-set-tags-to
(org-set-tags-to tags)))
(defun org-caldav-insert-org-entry--wrapup ()
"Helper function to finish inserting an org entry or todo.
Sets the block's tags, and return its MD5."
(org-back-to-heading)
(org-caldav--org-set-tags-to org-caldav-select-tags)
(md5 (buffer-substring-no-properties
(org-entry-beginning-position)
(org-entry-end-position))))
(defun org-caldav-scheduled-from-deadline (backend)
"Create a scheduled entry from deadline."
(when (eq backend 'icalendar)
(org-map-entries
(lambda ()
(let* ((sched (org-element-property :scheduled (org-element-at-point)))
(ts (org-element-property :deadline (org-element-at-point)))
(raw (org-element-property :raw-value ts))
(wu (org-element-property :warning-unit ts))
(wv (org-element-property :warning-value ts))
(dip (when org-caldav-days-in-past (* (abs org-caldav-days-in-past) -1)))
(stamp (org-entry-get nil "DEADLINE")))
;; skip if too old:
(unless (and dip stamp (> dip (org-time-stamp-to-now stamp)))
(when (and ts (not sched))
(org--deadline-or-schedule nil 'scheduled raw)
(search-forward "SCHEDULED: ")
(forward-char)
(if wv
(progn
(cond ((eq wu 'week) (setq wu 'day wv (* wv 7)))
((eq wu 'hour) (setq wu 'minute wv (* wv 60))))
(org-timestamp-change (* wv -1) wu))
(org-timestamp-change (* org-deadline-warning-days -1) 'day))))
(org-back-to-heading)
(org-caldav-debug-print 2 (format "scheduled: %s" (org-entry-get nil
"SCHEDULED" t))))))))
(defun org-caldav-set-org-tags (tags)
"Set tags to the headline, where tags is a coma-seperated
string. This comes from the ical CATEGORIES line."
(save-excursion
(org-back-to-heading)
(if (> (length tags) 0)
(let (cleantags)
(dolist (i (split-string tags ","))
(setq cleantags (cons
(replace-regexp-in-string " " "-" (string-trim i))
cleantags)))
(org-caldav--org-set-tags-to (reverse cleantags)))
(org-caldav--org-set-tags-to nil))))
(defun org-caldav-create-time-range (start-d start-t end-d end-t e-type)
"Create an Org timestamp range from START-D/T, END-D/T."
(with-temp-buffer
(cond
((string= "S" e-type) (insert "SCHEDULED: "))
((string= "DL" e-type) (insert "DEADLINE: ")))
(org-caldav-insert-org-time-stamp start-d start-t)
(if (and end-d
(not (equal end-d start-d)))
(progn
(insert "--")
(org-caldav-insert-org-time-stamp end-d end-t))
(when end-t
;; Same day, different time.
(backward-char 1)
(insert "-" end-t)))
(buffer-string)))
(defun org-caldav-insert-org-time-stamp (date &optional time)
"Insert org time stamp using DATE and TIME at point.
DATE is given as european date (DD MM YYYY)."
(insert
(concat "<" (org-caldav-convert-to-org-time date time) ">")))
(defun org-caldav-convert-to-org-time (date &optional time)
"Convert to org time stamp using DATE and TIME.
DATE is given as european date \"DD MM YYYY\"."
(let* ((stime (when time (mapcar 'string-to-number
(split-string time ":"))))
(hours (if time (car stime) 0))
(minutes (if time (nth 1 stime) 0))
(sdate (org-caldav--convert-to-calendar date))
(internaltime (encode-time 0 minutes hours
(calendar-extract-day sdate)
(calendar-extract-month sdate)
(calendar-extract-year sdate))))
(if time
(format-time-string "%Y-%m-%d %a %H:%M" internaltime)
(format-time-string "%Y-%m-%d %a" internaltime))))
(defun org-caldav--convert-to-calendar (date)
"Convert DATE to calendar.el-style list (month day year).
DATE is given as european date \"DD MM YYYY\"."
(let ((sdate (mapcar 'string-to-number (split-string date))))
(list (nth 1 sdate) (nth 0 sdate) (nth 2 sdate))))
(defun org-caldav-save-sync-state ()
"Save org-caldav sync database to disk.
See also `org-caldav-save-directory'."
(with-temp-buffer
(insert ";; This is the sync state from org-caldav\n;; calendar-id: "
org-caldav-calendar-id "\n;; Do not modify this file.\n\n")
(insert "(setq org-caldav-event-list\n'")
(let ((print-length nil)
(print-level nil))
(prin1 (delq nil
(mapcar (lambda (ev) (unless (eq (org-caldav-event-status ev) 'error) ev))
org-caldav-event-list))
(current-buffer)))
(insert ")\n")
;; This is just cosmetics.
(goto-char (point-min))
(while (re-search-forward ")[^)]" nil t)
(insert "\n"))
;; Save the current value of org-caldav-files
(insert "(setq org-caldav-previous-files '"
(let ((print-length nil)
(print-level nil))
(prin1-to-string org-caldav-files))
")\n")
;; Save it.
(write-region (point-min) (point-max)
(org-caldav-sync-state-filename org-caldav-calendar-id))))
(defun org-caldav-load-sync-state ()
"Load org-caldav sync database from disk."
(let ((filename (org-caldav-sync-state-filename org-caldav-calendar-id)))
(when (file-exists-p filename)
(with-temp-buffer
(insert-file-contents filename)
(eval-buffer)))))
(defun org-caldav-sync-state-filename (id)
"Return filename for saving the sync state of calendar with ID."
(expand-file-name
(concat "org-caldav-" (substring (md5 id) 1 8) ".el")
org-caldav-save-directory))
(defvar org-caldav-sync-results-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [(tab)] 'forward-button)
(define-key map [(backtab)] 'backward-button)
map)
"Keymap for org-caldav result buffer.")
(defun org-caldav-display-sync-results ()
"Display results of sync in a buffer."
(with-current-buffer (get-buffer-create "*org caldav sync result*")
(setq buffer-read-only nil)
(erase-buffer)
(insert "CalDAV Sync finished.\n\n")
(if (null org-caldav-sync-result)
(insert "Nothing was done.")
(insert "== Sync errors: \n\n")
(org-caldav-sync-result-print-entries
(org-caldav-sync-result-filter-errors))
(insert "\n== Successful syncs: \n\n")
(org-caldav-sync-result-print-entries
(org-caldav-sync-result-filter-errors t)))
(if (fboundp 'pop-to-buffer-same-window)
(pop-to-buffer-same-window (current-buffer))
(pop-to-buffer (current-buffer)))
(setq buffer-read-only t)
(goto-char (point-min))
(view-mode-enter)
(use-local-map org-caldav-sync-results-mode-map)))
(defun org-caldav-sync-result-filter-errors (&optional complement)
"Return items from sync results with errors.
If COMPLEMENT is non-nil, return all item without errors."
(delq nil
(mapcar
(lambda (x)
(if (string-match "^error" (symbol-name (car (last x))))
(unless complement x)
(when complement x)))
org-caldav-sync-result)))
(defun org-caldav-sync-result-print-entries (entries)
"Helper function to print ENTRIES."
(if (null entries)
(insert "None.\n")
(dolist (entry entries)
(let ((deleted (or (eq (nth 2 entry) 'deleted-in-org)
(eq (nth 2 entry) 'deleted-in-cal))))
(insert "UID: ")
(let ((start (point)))
(insert (nth 1 entry))
(unless deleted
(make-button start (point)
'action (lambda (&rest _ignore)
(org-caldav-goto-uid))
'follow-link t)))
(when (and (eq org-caldav-show-sync-results 'with-headings)
(not deleted))
(insert "\n Title: "
(or (org-caldav-get-heading-from-uid (nth 1 entry))
"(no title)")))
(insert "\n Status: "
(symbol-name (nth 2 entry))
" Action: "
(symbol-name (nth 3 entry)))
(when org-caldav-calendars
(insert "\n Calendar: " (car entry)))
(insert "\n\n")))))
(defun org-caldav-get-heading-from-uid (uid)
"Get org heading from entry with UID."
(let ((marker (org-id-find uid t)))
(if (null marker)
"(Could not find UID)"
(with-current-buffer (marker-buffer marker)
(goto-char (marker-position marker))
(org-narrow-to-subtree)
(goto-char (point-min))
(org-caldav--org-show-subtree)
(prog1
(if (re-search-forward org-complex-heading-regexp nil t)
(match-string 4)
"(Could not find heading)")
(widen))))))
(defun org-caldav-goto-uid ()
"Jump to UID under point."
(when (button-at (point))
(beginning-of-line)
(looking-at "UID: \\(.+\\)$")
(org-id-goto (match-string 1))))
(defun org-caldav-get-calendar-summary-from-uid (uid)
"Get summary from UID from calendar."
(let ((buf (org-caldav-get-event uid))
(heading ""))
(when buf
(with-current-buffer buf
(goto-char (point-min))
(when (re-search-forward "^SUMMARY:\\(.*\\)$" nil t)
(setq heading (match-string 1)))))
heading))
(defun org-caldav--datetime-to-colontime (datetime e property &optional default)
"Extract time part of decoded datetime.
If PROPERTY in event E contains has valuetype \"DATE\" instead of
\"DATE-TIME\", return DEFAULT instead."
(if (and datetime
(not (string=
(cadr (icalendar--get-event-property-attributes
e property))
"DATE")))
(icalendar--datetime-to-colontime datetime)
default))
(defun org-caldav--event-date-plist (e property zone-map)
"Helper function for `org-caldav-convert-event-or-todo'.
Extracts datetime-related attributes from PROPERTY of event E and
puts them in a plist."
(let* ((dt-prop (icalendar--get-event-property e property))
(dt-zone (icalendar--find-time-zone
(icalendar--get-event-property-attributes
e property)
zone-map))
(dt-dec (icalendar--decode-isodatetime dt-prop nil dt-zone)))
(list 'event-property dt-prop
'zone dt-zone
'decoded dt-dec
'date (icalendar--datetime-to-diary-date dt-dec)
'time (org-caldav--datetime-to-colontime dt-dec e property))))
(defun org-caldav--icalendar--all-todos (icalendar)
"Return the list of all existing todos in the given ICALENDAR."
(let ((result '()))
(mapc (lambda (elt)
(setq result (append (icalendar--get-children elt 'VTODO)
result)))
(nreverse icalendar))
result))
;; The following is taken from icalendar.el, written by Ulf Jasper.
;; The LOCATION property is added the extracted list
(defun org-caldav-convert-event-or-todo (is-todo)
"Convert icalendar event or todo in current buffer.
If IS-TODO, it is a VTODO, else a VEVENT. Returns an alist of properties
which can be fed into `org-caldav-insert-org-event-or-todo'."
(let ((decoded (decode-coding-region (point-min) (point-max) 'utf-8 t)))
(erase-buffer)
(set-buffer-multibyte t)
(setq buffer-file-coding-system 'utf-8)
(insert decoded))
(goto-char (point-min))
(let* ((calendar-date-style 'european)
(ical-list (icalendar--read-element nil nil))
(e (car (if is-todo
(org-caldav--icalendar--all-todos ical-list)
(icalendar--all-events ical-list))))
(zone-map (icalendar--convert-all-timezones ical-list))
(dtstart-plist (org-caldav--event-date-plist e 'DTSTART zone-map))
(eventdata-alist
`((start-d . ,(plist-get dtstart-plist 'date))
(start-t . ,(plist-get dtstart-plist 'time))
(dtstart-dec . ,(plist-get dtstart-plist 'decoded))
(summary . ,(icalendar--convert-string-for-import
(or (icalendar--get-event-property e 'SUMMARY)
"No Title")))
(description . ,(icalendar--convert-string-for-import
(or (icalendar--get-event-property e 'DESCRIPTION)
""))))))
(if is-todo
(org-caldav-convert-event-or-todo--todo e zone-map eventdata-alist)
(org-caldav-convert-event-or-todo--event e zone-map eventdata-alist))))
(defun org-caldav-convert-event-or-todo--event (e zone-map eventdata-alist)
"Helper function of `org-caldav-event-or-todo' to handle VEVENT."
(let* ((start-d (cdr (assq 'start-d eventdata-alist)))
(start-t (cdr (assq 'start-t eventdata-alist)))
(dtstart-dec (cdr (assq 'dtstart-dec eventdata-alist)))
(summary (cdr (assq 'summary eventdata-alist)))
(dtend-plist (org-caldav--event-date-plist e 'DTEND zone-map))
(dtend-dec (plist-get dtend-plist 'decoded))
(dtend-1-dec (icalendar--decode-isodatetime
(plist-get dtend-plist 'event-property) -1
(plist-get dtend-plist 'zone)))
e-type
(duration (icalendar--get-event-property e 'DURATION)))
(when (string-match "^\\(?:\\(DL\\|S\\):\s+\\)?\\(.*\\)$" summary)
(setq e-type (match-string 1 summary))
(setq summary (match-string 2 summary)))
(when duration
(let ((dtend-dec-d (icalendar--add-decoded-times
dtstart-dec
(icalendar--decode-isoduration duration)))
(dtend-1-dec-d (icalendar--add-decoded-times
dtstart-dec
(icalendar--decode-isoduration duration
t))))
(if (and dtend-dec (not (eq dtend-dec dtend-dec-d)))
(message "Inconsistent endtime and duration for %s"
summary))
(setq dtend-dec dtend-dec-d)
(setq dtend-1-dec dtend-1-dec-d)))
(let ((end-t (org-caldav--datetime-to-colontime
dtend-dec e 'DTEND start-t)))
;; Return result
(append `((component-type . event)
(end-d
. ,(if end-t
(if dtend-dec
(icalendar--datetime-to-diary-date dtend-dec)
start-d)
(if dtend-1-dec
(icalendar--datetime-to-diary-date dtend-1-dec)
start-d)))
(end-t . ,end-t)
(location
. ,(icalendar--convert-string-for-import
(or (icalendar--get-event-property e 'LOCATION) "")))
(end-type . ,e-type))
eventdata-alist))))
(defun org-caldav-convert-event-or-todo--todo (e zone-map eventdata-alist)
"Helper function of `org-caldav-event-or-todo' to handle VTODO."
(let* ((dtdue-plist (org-caldav--event-date-plist e 'DUE zone-map))
(dtcomplete-plist (org-caldav--event-date-plist
e 'COMPLETED zone-map))
(percent-complete (icalendar--get-event-property e 'PERCENT-COMPLETE))
(stat (icalendar--get-event-property e 'STATUS)))
(unless percent-complete
(setq percent-complete
(cond
((string= stat "NEEDS-ACTION") "0")
((string= stat "IN-PROCESS") "50")
((string= stat "COMPLETED") "100")
(t "0"))))
(append `((component-type . todo)
(due-d . ,(plist-get dtdue-plist 'date))
(due-t . ,(plist-get dtdue-plist 'time))
(priority . ,(icalendar--get-event-property e 'PRIORITY))
(percent-complete ., percent-complete)
(status . ,stat)
(completed-d . ,(plist-get dtcomplete-plist 'date))
(completed-t . ,(plist-get dtcomplete-plist 'time))
(categories . ,(icalendar--convert-string-for-import
(or (icalendar--get-event-property e 'CATEGORIES)
""))))
eventdata-alist)))
;; This is adapted from url-dav.el, written by Bill Perry.
;; This does more error checking on the headers and retries
;; in case of an error.
(defun org-caldav-save-resource (url obj)
"Save string OBJ as URL using WebDAV.
This switches to OAuth2 if necessary."
(let* ((counter 0)
errormessage full-response buffer)
(while (and (not buffer)
(< counter org-caldav-retry-attempts))
(with-current-buffer
(org-caldav-url-retrieve-synchronously
url "PUT" obj
'(("Content-type" . "text/calendar; charset=UTF-8")))
(goto-char (point-min))
(if (looking-at "HTTP.*2[0-9][0-9]")
(setq buffer (current-buffer))
;; There was an error putting the resource, try again.
(when (> org-caldav-debug-level 1)
(setq full-response (buffer-string)))
(setq errormessage (buffer-substring (point-min) (line-end-position)))
(setq counter (1+ counter))
(org-caldav-debug-print
1 (format "(Try %d) Error when trying to put URL %s: %s"
counter url errormessage))
(kill-buffer))))
(if buffer
(kill-buffer buffer)
(org-caldav-debug-print
1
(format "Failed to put URL %s after %d tries with error %s"
url org-caldav-retry-attempts errormessage))
(org-caldav-debug-print
2
(format "Full error response:\n %s" full-response)))
(< counter org-caldav-retry-attempts)))
;;;###autoload
(defun org-caldav-import-ics-buffer-to-org ()
"Add ics content in current buffer to `org-caldav-inbox'."
(let ((event (org-caldav-convert-event-or-todo nil))
(file (org-caldav-inbox-file org-caldav-inbox)))
(with-current-buffer (find-file-noselect file)
(let* ((point-and-level (org-caldav-inbox-point-and-level
org-caldav-inbox event))
(point (car point-and-level))
(level (cdr point-and-level)))
(goto-char point)
(org-caldav-insert-org-event-or-todo
(append event `((uid . nil) (level . ,level))))
(message "%s: Added event: %s"
file
(buffer-substring
point
(save-excursion
(goto-char point)
(line-end-position 2))))))))
;;;###autoload
(defun org-caldav-import-ics-to-org (path)
"Add ics content in PATH to `org-caldav-inbox'."
(with-current-buffer (get-buffer-create "*import-ics-to-org*")
(delete-region (point-min) (point-max))
(insert-file-contents path)
(org-caldav-import-ics-buffer-to-org)))
(provide 'org-caldav)
;;; org-caldav.el ends here