pax_global_header00006660000000000000000000000064145040535140014513gustar00rootroot0000000000000052 comment=1bbce67fa64cafbc1b50eb3ecd54e48c797f8437 org-caldav-master/000077500000000000000000000000001450405351400143675ustar00rootroot00000000000000org-caldav-master/.gitignore000066400000000000000000000000071450405351400163540ustar00rootroot00000000000000*.elc org-caldav-master/License.txt000066400000000000000000001045051450405351400165170ustar00rootroot00000000000000 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.org000066400000000000000000000517001450405351400160400ustar00rootroot00000000000000#+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.el000066400000000000000000001160651450405351400207700ustar00rootroot00000000000000;; 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.el000066400000000000000000002716651450405351400167510ustar00rootroot00000000000000;;; 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 "\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