├── .gitignore ├── License.txt ├── Readme.org ├── doc ├── org-caldav.org └── org-caldav.texi ├── org-caldav-tests.el └── org-caldav.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Readme.org: -------------------------------------------------------------------------------- 1 | #+TITLE: org-caldav 2 | 3 | Caldav sync for Emacs Orgmode 4 | 5 | For documentation, see the manual at [[file:doc/org-caldav.org]]. The 6 | manual is also available at 7 | [[https://jackkamm.neocities.org/org-caldav-manual]]. 8 | -------------------------------------------------------------------------------- /doc/org-caldav.org: -------------------------------------------------------------------------------- 1 | #+TITLE: org-caldav 2 | 3 | #+TEXINFO_DIR_CATEGORY: Emacs 4 | #+TEXINFO_DIR_TITLE: Org-caldav: (org-caldav). 5 | #+TEXINFO_DIR_DESC: Caldav sync for Emacs Orgmode 6 | 7 | Caldav sync for Emacs Orgmode 8 | 9 | * Prerequisites 10 | 11 | - Emacs >= 26.3 12 | - Org >= 9.1 13 | - [[#caldav-servers][Compatible CalDav server]] 14 | 15 | *IMPORTANT*: Before using this code, please make sure you have backups 16 | of your precious Org files. Also, it is recommended to create a new, 17 | empty calendar on your server for using this package. 18 | 19 | *ALSO IMPORTANT*: When using this package, possibly all Org entries will 20 | get an UID property (see the docstring of ~org-icalendar-store-UID~ for 21 | further details). If you don't want this, then /do not use this 22 | package/; there is just no way around that. It is the only reliable way 23 | to uniquely identify Org entries. 24 | 25 | * Basic setup 26 | :PROPERTIES: 27 | :CUSTOM_ID: setup 28 | :END: 29 | 30 | At a minimum, set ~org-caldav-url~, ~org-caldav-calendar-id~, 31 | ~org-caldav-inbox~, ~org-caldav-files~, and ~org-icalendar-timezone~ 32 | ([[#required-configs][Required configuration settings]]). 33 | 34 | ** Minimal example 35 | 36 | This example configures a single Org file =/path/to/inbox.org= to sync 37 | with a single calendar (with ID =CALENDAR-ID=) located on a Nextcloud 38 | server at =NEXTCLOUD-SERVER-URL=. 39 | 40 | #+begin_src emacs-lisp 41 | (require 'org-caldav) 42 | 43 | ;; URL of the caldav server 44 | (setq org-caldav-url "https://NEXTCLOUD-SERVER-URL/remote.php/dav/calendars/USERID") 45 | 46 | ;; calendar ID on server 47 | (setq org-caldav-calendar-id "CALENDAR-ID") 48 | 49 | ;; Org filename where new entries from calendar stored 50 | (setq org-caldav-inbox "/path/to/inbox.org") 51 | 52 | ;; Additional Org files to check for calendar events 53 | (setq org-caldav-files nil) 54 | 55 | ;; Usually a good idea to set the timezone manually 56 | (setq org-icalendar-timezone "Europe/Berlin") 57 | #+end_src 58 | 59 | ** Required configuration settings 60 | :PROPERTIES: 61 | :CUSTOM_ID: required-configs 62 | :END: 63 | 64 | - Set ~org-caldav-url~ to the base address of your CalDAV server: 65 | 66 | - Owncloud/Nextcloud (9.x and above): 67 | https://OWNCLOUD-SERVER-URL/remote.php/dav/calendars/USERID 68 | - Google: Set to symbol ~'google~. See [[#gcal-sync][Syncing to Google Calendar]] 69 | for additional required setup. 70 | 71 | - Set ~org-caldav-calendar-id~ to the calendar ID of your new calendar: 72 | 73 | - Own/NextCloud: Click on that little symbol next to the calendar name 74 | and inspect the link of the calendar; the last element of the shown 75 | path is the calendar-id. This should /usually/ be the same as the 76 | name of the calendar, but not necessarily: Owncloud might replace 77 | certain characters (upper to lowercase, for instance), or it might 78 | even be entirely different if the calendar was created by another 79 | CalDAV application. 80 | - Google: Click on 'calendar settings' and the id will be shown next 81 | to "Calendar Address". It is of the form 82 | ~ID@group.calendar.google.com~. Do /not/ omit the domain! 83 | 84 | - Set ~org-caldav-inbox~ to an org filename where new entries from the 85 | calendar should be stored. Just to be safe, it's a good idea to use 86 | an empty, dedicated Org file for that. 87 | 88 | - Set ~org-caldav-files~ to the list of org files you would like to 89 | sync. The above ~org-caldav-inbox~ will be automatically added, so you 90 | don't have to add it here. 91 | 92 | - It is usually a good idea to manually set ~org-icalendar-timezone~ to 93 | the timezone of your remote calendar. It should be a simple string 94 | like "Europe/Berlin". If that doesn't work and your events are 95 | shifted by a few hours, try the setting "UTC" (the SOGo calendar 96 | server seems to need this). 97 | 98 | * Usage 99 | 100 | First, create a calendar on the CalDav server, and configure which Org 101 | files to sync it with (see [[#setup][Basic setup]]). 102 | 103 | Then, do: 104 | 105 | ~M-x org-caldav-sync~ 106 | 107 | to sync between Org and CalDav. 108 | 109 | You will be prompted to manually enter the username/password on each 110 | sync; see [[#authinfo][Storing authentication information]] on how to save the 111 | password and avoid manual entry. 112 | 113 | If you have many calendar items, the first sync can easily take 114 | several minutes, especially if using a slow CalDav implementation like 115 | Google's. If you have to abort the initial sync for some reason, just 116 | start ~org-caldav-sync~ again in the same Emacs session and you should 117 | get asked if you'd like to resume. Likewise for any errors -- 118 | especially when using Google Calendar, it is not unusual to get stuff 119 | like '409' errors during the initial sync. Just run ~org-caldav-sync~ 120 | again until all events are uploaded. 121 | 122 | * Advanced configuration 123 | :PROPERTIES: 124 | :CUSTOM_ID: advanced-config 125 | :END: 126 | 127 | Before reading this section, first consult the section on [[#setup][Basic setup]] 128 | (and in particular [[#required-configs][Required configuration settings]]). 129 | 130 | Additional, advanced configuration options are listed below in this 131 | section. 132 | 133 | Note that org-caldav uses [[https://orgmode.org/manual/iCalendar-Export.html][ox-icalendar.el]] to export from Org to 134 | iCalendar, so it's worth checking the options there as well. For 135 | example, use ~org-icalendar-alarm-time~ to add a reminder to your 136 | entries. 137 | 138 | ** Sync direction (one-way sync) 139 | 140 | By default, org-caldav does two-way syncing, that means it does not 141 | matter where and how you change an entry. You can also move Org 142 | entries freely from one file to another, as long as they are all 143 | listed in ~org-caldav-files~. 144 | 145 | To do one-way sync only, set ~org-caldav-sync-direction~ to 146 | ~'org->cal~ or ~'cal->org~, depending on which direction you'd like to 147 | have. If you choose ~'org->cal~, then ~org-caldav-inbox~ won't matter 148 | and can be ~nil~. Likewise, if you choose ~'cal->org~, then 149 | ~org-caldav-files~ will be ignored and only the calendar will be 150 | imported into the inbox. 151 | 152 | WARNING: It is NOT safe to switch the same calendar between 1-way and 153 | 2-way sync modes. Doing so may cause unexpected behavior, such as 154 | deleting all events in the calendar. 155 | 156 | ** Filtering entries 157 | :PROPERTIES: 158 | :CUSTOM_ID: filter-entries 159 | :END: 160 | 161 | There are several possibilities to choose which entries should be 162 | synced and which not: 163 | 164 | - If you only want to sync manually marked entries, use 165 | ~org-caldav-select-tags~, which is directly mapped to 166 | ~org-export-select-tags~, so see its doc-string on how it works. 167 | 168 | - If you want to exclude certain tags, use ~org-caldav-exclude-tags~, 169 | which is mapped to ~org-icalendar-exclude~ tags. 170 | 171 | - If you want more fine grained control, use 172 | ~org-caldav-skip-conditions~. The syntax of the conditions is 173 | described in the doc-string of ~org-agenda-skip-if~. 174 | 175 | - In case you just want to keep your remote calendar clean, set 176 | ~org-caldav-days-in-past~ to the number of days you want to keep in 177 | the past on the remote calendar. This does not affect your org files, 178 | it works just as a filter for entries older than N days. 179 | 180 | Note however that the normal ~org-agenda-skip-function(-global)~ will 181 | *not* have any effect on the icalendar exporter (this used to be the 182 | case, but changed with the new exporters). 183 | 184 | ** Syncing deletions 185 | 186 | If you delete entries in your Org files, the corresponding iCalendar 187 | entries will by default get deleted. You can change that behavior with 188 | ~org-caldav-delete-calendar-entries~ to never delete, or to ask before 189 | deletion. 190 | 191 | You must be careful to not simply remove previously synced files from 192 | ~org-caldav-files~, as org-caldav would view all the entries from those 193 | files as deleted and hence by default also delete them from the 194 | calendar. However, org-caldav should be able to detect this situation 195 | and warn you with the message 'Previously synced file(s) are missing', 196 | asking you whether to continue nonetheless. 197 | 198 | If you delete events in your calendar, you will by default get asked 199 | if you'd like to delete the corresponding Org event. You can change 200 | that behavior through ~org-caldav-delete-org-entries~. 201 | 202 | If you answer a deletion request with "no", the event should get 203 | re-synced to the calendar next time you call ~org-caldav-sync~. 204 | 205 | ** Storing authentication information in authinfo/netrc 206 | :PROPERTIES: 207 | :CUSTOM_ID: authinfo 208 | :END: 209 | 210 | If you don't want to enter your user/password every time, you can 211 | store it permanently in an authinfo file. In Emacs, the auth-source 212 | package takes care of that, but the syntax for https authentication is 213 | a bit peculiar. You have to use a line like the following 214 | 215 | #+begin_example 216 | machine www.google.com:443 port https login username password secret 217 | #+end_example 218 | 219 | Note that you have to specify the port number in the URL and /also/ 220 | specify 'https' for the port. This is not a bug. For more information, 221 | see (info "auth"), especially section "Help for users". 222 | 223 | Since you are storing your password in a file you should encrypt it 224 | using GnuPG. Emacs will prompt you for a decryption key when it tries 225 | to read the file. 226 | 227 | ** Syncing with more than one calendar 228 | :PROPERTIES: 229 | :CUSTOM_ID: sync-multiple 230 | :END: 231 | 232 | This can be done by setting the variable ~org-caldav-calendars~. It 233 | should be a list of plists (a 'plist' is simply a list with alternating 234 | :key's and values). Through these plists, you can override the global 235 | values of variables like ~org-caldav-calendar-id~, and calling 236 | ~org-caldav-sync~ will go through these plists in order. 237 | 238 | Example: 239 | 240 | #+begin_src emacs-lisp 241 | (setq org-caldav-calendars 242 | '((:calendar-id "work@whatever" :files ("~/org/work.org") 243 | :inbox "~/org/fromwork.org") 244 | (:calendar-id "stuff@mystuff" 245 | :files ("~/org/sports.org" "~/org/play.org") 246 | :skip-conditions (regexp "soccer") 247 | :inbox "~/org/fromstuff.org")) ) 248 | #+end_src 249 | 250 | This means that you have two calendars with IDs "work@whatever" and 251 | "stuff@mystuff". Both will be accessed through the global value of 252 | org-caldav-url, since the key :url isn't specified. The calendar 253 | "work@whatever" will be synced with the file 'work.org' and inbox 254 | 'fromwork.org', while "stuff@mystuff" with 'sports.org' and 255 | 'play.org', /unless/ there's the string 'soccer' in the heading, and 256 | and inbox is 'fromstuff.org'. See the doc-string of 257 | ~org-caldav-calendars~ for more details on which keys you can use. 258 | 259 | ** Customizing the inbox 260 | :PROPERTIES: 261 | :CUSTOM_ID: custom-inbox 262 | :END: 263 | 264 | See the doc-string of ~org-caldav-inbox~ if you want more flexibility in 265 | where new items should be put. Instead of simply providing a file, you 266 | can also choose an existing entry or headline, or put the entry under a 267 | datetree. 268 | 269 | ** Syncing TODOs between Org and CalDav 270 | :PROPERTIES: 271 | :CUSTOM_ID: sync-todo 272 | :END: 273 | 274 | This feature is relatively new and less well tested, so it is 275 | recommended to have backups before using it. It has been tested on 276 | nextcloud and radicale. 277 | 278 | To sync TODO's between Org and the CalDav server, do: 279 | 280 | #+begin_src emacs-lisp 281 | (setq org-icalendar-include-todo 'all 282 | org-caldav-sync-todo t) 283 | #+end_src 284 | 285 | The first instructs the Org exporter to include TODOs; the second 286 | tells org-caldav to import icalendar VTODOs as Org TODOs. 287 | 288 | Other customizations to consider (see their documentation for more 289 | details): 290 | 291 | - ~org-caldav-todo-priority~ to control how priority levels map between 292 | iCalendar and Org. 293 | - ~org-caldav-todo-percent-states~ to convert between 294 | ~org-todo-keywords~ and iCalendar's percent-complete property. 295 | - ~org-caldav-todo-deadline-schedule-warning-days~ to auto-create 296 | SCHEDULED timestamps when a DEADLINE is present (this might be useful 297 | for users of the OpenTasks app). 298 | 299 | If you find that some Org entries get an extra tag which equals their 300 | CATEGORY, this might be caused by the CATEGORY being exported to 301 | iCalendar, and then re-imported to Org as a tag. In that case, do 302 | 303 | #+begin_src emacs-lisp 304 | (setq org-icalendar-categories '(local-tags)) 305 | #+end_src 306 | 307 | to prevent the CATEGORY from being exported to iCalendar. This problem 308 | only seems to affect some CalDav servers: in particular, NextCloud 309 | is affected, but Radicale does not seem to experience this problem. 310 | 311 | ** Behavior of recurring TODO deadlines without a start time 312 | :PROPERTIES: 313 | :CUSTOM_ID: recur-deadline 314 | :END: 315 | 316 | Technically, the [[https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.4][iCalendar spec]] requires repeating events and todos 317 | (i.e. having an ~RRULE~ property) to have a starting time (~DTSTART~ 318 | in iCalendar, equivalent to ~SCHEDULED~ in Org TODOs). This means 319 | that, a TODO with a repeating ~DEADLINE~ but without a ~SCHEDULED~ 320 | property, such as below, is not allowed by the iCalendar spec: 321 | 322 | #+begin_src org 323 | ,* TODO An example todo with a repeating deadline and no start time 324 | DEADLINE: <2024-09-15 Sun +1w> 325 | #+end_src 326 | 327 | This is a clear shortcoming of the iCalendar spec, because it /does/ 328 | allow tasks to have a standalone deadline without a starting time, but 329 | /doesn't/ allow such tasks to repeat. 330 | 331 | By default, ~ox-icalendar~ follows the iCalendar spec, and when 332 | exporting a TODO with a repeating ~DEADLINE~ but no ~SCHEDULED~ 333 | timestamp, will add a start time based on 334 | ~org-deadline-warning-days~. On future syncs, this start time will be 335 | inserted into Org as a ~SCHEDULED~ timestamp. 336 | 337 | However, in practice, many iCalendar implementations ignore this 338 | limitation, and allow Todos with ~DEADLINE~ (~DUE~) times to have 339 | repeaters (~RRULE~), even if they are missing ~SCHEDULED~ (~DTSTART~) 340 | times. If your CalDav server allows this, then you may set the 341 | variable ~org-icalendar-todo-unscheduled-start~ to ~nil~. This will 342 | prevent ~ox-icalendar~ from adding a start time to such TODOs, thus 343 | preventing the ~SCHEDULED~ timestamp from being inserted on future 344 | syncs. 345 | 346 | See [[#recur-event-todo][Repeating events and todos]] for more details about how org-caldav 347 | handles repeating events and todos. 348 | 349 | * Compatible CalDav servers 350 | :PROPERTIES: 351 | :CUSTOM_ID: caldav-servers 352 | :END: 353 | 354 | - *Owncloud* and *Nextcloud*: Regularly tested. 355 | 356 | - *Radicale* and *Baikal*: Works. If you get problems with 'Digest' 357 | authentication, switch back to 'Basic' (make sure to use https, 358 | though!). If you get asked for password repeatedly, put it in 359 | ~.authinfo~ file ([[#authinfo][Storing authentication information]]). 360 | 361 | - *SOGo* and *Kolab*: Reported to be working 362 | (https://docs.kolab.org/client-configuration/emacs.html) 363 | 364 | - *Google Calendar*: Should work, but you need to register an 365 | application with the Google Developer Console for OAuth2 366 | authentication (see below), because Google explicitly forbids to put 367 | client id/secrets into open source software (see 368 | https://developers.google.com/terms, section 4b, paragraph 1). Instead 369 | of doing that though, I'd rather suggest you choose another service 370 | provider. 371 | 372 | ** Syncing to Google Calendar 373 | :PROPERTIES: 374 | :CUSTOM_ID: gcal-sync 375 | :END: 376 | 377 | NOTE: Using org-caldav with Google Calendar may be currently 378 | broken. See [[https://github.com/dengste/org-caldav/issues/284]] 379 | 380 | The CalDAV endpoint for Google Calendar requires OAuth2 381 | authentication. So first, you need to install the oauth2 library from 382 | GNU ELPA, and afterwards you need to acquire an application ID and 383 | secret from the Google Developer Console. For details on how to do 384 | this, follow the Google documentation at 385 | 386 | https://developers.google.com/google-apps/calendar/caldav/v2/guide#creating_your_client_id 387 | 388 | Put the client ID and secret into ~org-caldav-oauth2-client-id~ and 389 | ~org-caldav-oauth2-client-secret~, respectively. Then set 390 | ~org-caldav-url~ to the symbol ~'google~, and look up the 391 | ~org-caldav-calendar-id~ as described above. 392 | 393 | On first connection, the oauth2 library should redirect you to the 394 | Google OAuth2 authentication site. This requires a javascript enabled 395 | browser, so make sure that ~browse-url-browser-function~ is set to 396 | something like ~browse-url-firefox~ (the internal eww or w3m browsers 397 | will *not* work). After authentication, you will be given a key that 398 | you have to paste into the Emacs prompt. The oauth2 library will save 399 | this key in Emacs' secure plist store, which is encrypted with 400 | GnuPG. If you have not yet used a secure plist store, you will be 401 | asked for its encryption passphrase. In the future, you should only 402 | need to enter that passphrase again to connect with Google Calendar. 403 | 404 | By default, plstore will *not* cache your entered password, so it will 405 | possibly ask you *many* times. To activate caching, use 406 | 407 | #+begin_src emacs-lisp 408 | (setq plstore-cache-passphrase-for-symmetric-encryption t) 409 | #+end_src 410 | 411 | * Implementation details 412 | 413 | ** Org and the iCalendar format 414 | 415 | An Org entry can store much more information than an iCalendar entry, 416 | so there is no one-to-one correspondence between the two formats which 417 | makes syncing a bit difficult. 418 | 419 | - Org to iCalendar 420 | 421 | This package uses the org-icalendar package to do the export to the 422 | iCalendar format (.ics files). By default, it uses the title of the 423 | Org entry as SUMMARY and puts the entry's body into DESCRIPTION, 424 | snipping stuff like properties and timestamps (you can override that 425 | with properties of the same name, but IMO it makes stuff just more 426 | complicated). The variable ~org-icalendar-include-body~ 427 | denotes how many characters from the body should be included as 428 | DESCRIPTION (by default all characters are included). 429 | 430 | - iCalendar to Org 431 | 432 | If you create a new iCalendar entry in your calendar, you'll get an 433 | Org entry with SUMMARY as heading, DESCRIPTION as body and the 434 | timestamp. However, if you /change/ an existing entry in the calendar, 435 | things get more complicated and the variable 436 | ~org-caldav-sync-changes-to-org~ comes into play. Its default is the 437 | symbol "title-and-timestamp", which means that only the entry's 438 | heading is synced (with SUMMARY) and the timestamp gets updated, but 439 | /not/ the entry's body with DESCRIPTION. The simple reason is that 440 | you might loose data, since DESCRIPTION is rather limited in what it 441 | can store. Still, you can set the variable to the symbol "all", which 442 | will completely /replace/ an existing Org entry with the entry that 443 | gets generated from the calendar's event. You can also limit syncing 444 | to heading and/or timestamp only. 445 | 446 | To be extra safe, org-caldav will by default backup entries it 447 | changes. See the variable ~org-caldav-backup-file~ for details. 448 | 449 | - Org sexp entries 450 | 451 | A special case are sexp entries like 452 | 453 | #+begin_src org 454 | %%(diary-anniversary 2 2 1969) Foo's birthday 455 | 456 | ,* Regular meeting 457 | <%%(diary-float t 4 2)> 458 | #+end_src 459 | 460 | As you can see, they can appear in two different ways: plain by 461 | themselves, or inside an Org entry. If they are inside an Org entry, 462 | there's a good chance they will be exported (see below) and have an ID 463 | property, so they can be found by org-caldav. We can sync the title, 464 | but syncing the timestamp with the s-expression is just infeasible, so 465 | this will generate a sync error (which are /not/ critical; you'll just 466 | see them at the end of the sync, just so that you're aware that some 467 | stuff wasn't synced properly). 468 | 469 | However, sexp-entries are insanely flexible, and there are limits as 470 | to what the icalendar exporter will handle. For example, this here 471 | 472 | #+begin_src org 473 | ,** Regular event 474 | <%%(memq (calendar-day-of-week date) '(1 3 5))> 475 | #+end_src 476 | 477 | will not be exported at all. 478 | 479 | If the sexp entry is not inside an Org entry but stands by itself, 480 | they still will be exported, but they won't get an ID (since IDs are 481 | properties linked to Org entries). In practice, that means that you 482 | can delete and change them inside Org and this will be synced, but if 483 | you /change/ them in the /calendar/, this will /not/ get synced 484 | back. Org-caldav just cannot find those entries, so this will generate 485 | a one-time sync error instead (again: those are not critical, just 486 | FYI). If you don't want those entries to be exported at all, just set 487 | ~org-icalendar-include-sexps~ to nil. 488 | 489 | ** Conflict handling 490 | 491 | Now that's an easy one: Org always wins. That means, if you change an 492 | entry in Org /and/ in the calendar, the changes in the calendar will 493 | be lost. I might implement proper conflict handling some day, but 494 | don't hold your breath (patches are welcome, of course). 495 | 496 | ** Repeating events and todos 497 | :PROPERTIES: 498 | :CUSTOM_ID: recur-event-todo 499 | :END: 500 | 501 | Org-caldav has basic support for repeating events and todos. In 502 | particular, simple Org timestamp repeaters such as ~+3d~ or ~+1m~ can 503 | be succesfully sync'd bidirectionally. 504 | 505 | However, complex iCalender recurrences, such as "repeat on the 2nd 506 | Tuesday of each month until X date", are not supported. 507 | 508 | For Org TODOs with both ~SCHEDULED~ and ~DEADLINE~ timestamps, each of 509 | the timestamps must have the same repeater, otherwise the behavior is 510 | undefined. 511 | 512 | Furthermore, the behavior of Org TODOs with a repeating ~DEADLINE~ 513 | timestamp, but no ~SCHEDULED~ timestamp, has some subtleties; see the 514 | configuration section: [[#recur-deadline][Behavior of recurring TODO deadlines without a start time]]. 515 | 516 | ** How syncing happens (a.k.a. David's little CalDAV rant) 517 | 518 | (This is probably not interesting, so you can skip this.) 519 | 520 | CalDAV is a mess. 521 | 522 | First off, it is based on WebDAV, which has its own fair share of 523 | problems. The main design flaw of CalDAV however, is that UID and 524 | resource name (the "filename", if you want) are two different 525 | things. I know that there are reasons for that (not everything has a 526 | UID, like timezones, and you can put several events in one resource), 527 | but this is typical over-engineering to allow some marginal use cases 528 | pretty much no one needs. Another problem is that you have to do 529 | additional round-trips to get Etag and sequence number, which makes 530 | CalDAV pretty slow. 531 | 532 | Org-caldav takes the easy route: it assumes that every resource 533 | contains one event, and that UID and resource name are identical. In 534 | fact, Google's CalDAV interface even enforces the latter. And while 535 | Owncloud does not enforce it, at least it just does it if you create 536 | items in its web interface. 537 | 538 | However, the CalDAV standard does not demand this, so I guess there 539 | are servers out there with which org-caldav does not work. Patches 540 | welcome. 541 | 542 | Now, all this would be bad enough if it weren't for the sloppy server 543 | implementations which make implementing a CalDAV client a living hell 544 | and led to several rewrites of the code. Especially Google, the 500 545 | pound gorilla in the room, doesn't really care much for CalDAV. I 546 | guess they really like their own shiny REST-based calendar API better, 547 | and I can't blame them for that. 548 | 549 | * Tips, Tricks, and Troubleshooting 550 | 551 | ** Standalone import of ICS files 552 | 553 | org-caldav can also be used for standalone import of ICS files to Org. 554 | 555 | In particular, see ~org-caldav-import-ics-buffer-to-org~ to import 556 | iCalendar entries (e.g. from e-mail attachments) directly to your 557 | ~org-caldav-inbox~. 558 | 559 | Also, see ~org-caldav-convert-ics-to-datetree~ to convert an iCalendar 560 | file into an Org datetree in a separate buffer (use 561 | ~org-caldav-datetree-treetype~ to control the style of datetree). 562 | 563 | ** Storage of sync information and sync from different computers 564 | 565 | The current sync state is stored in a file ~org-caldav-SOMEID.el~ in 566 | the ~/.emacs.d directory. You can change the location through the 567 | variable ~org-caldav-save-directory~. SOMEID directly depends on the 568 | calendar id (it's a snipped MD5). 569 | 570 | If you sync your Org files across different machines and want to use 571 | org-caldav on all of them, don't forget to sync the org sync state, 572 | too. Probably your best bet is to set ~org-caldav-save-directory~ to the 573 | path you have your Org files in, so that it gets copied alongside with 574 | them. 575 | 576 | ** Starting from scratch 577 | 578 | If your sync state somehow gets broken, you can make a clean slate by 579 | doing 580 | 581 | #+begin_example 582 | C-u M-x org-caldav-delete-everything 583 | #+end_example 584 | 585 | The function has to be called with a prefix so that you don't call it 586 | by accident. This will delete everything in the calendar along with 587 | the current sync state. You can then call ~org-caldav-sync~ afterwards 588 | and it will completely put all Org events into the now empty 589 | calendar. Needless to say, don't do that if you have new events in 590 | your calendar which are not synced yet... 591 | 592 | Deleting many events can be slow, though; in that case, just delete 593 | the calendar and re-create it, delete the sync state file in 594 | ~/.emacs.d and restart Emacs. 595 | 596 | ** Timezone problems 597 | 598 | Timezone handling is plain horrible, and it seems every CalDAV server 599 | does it slightly differently, also using non-standard headers like 600 | X-WR-TIMEZONE. If you see items being shifted by a few hours, make 601 | really really sure you have properly set ~org-icalendar-timezone~, and 602 | that your calendar is configured to use the same one. 603 | 604 | If it still does not work, you can try setting ~org-icalendar-timezone~ 605 | to the string "UTC". This will put all events using UTC times and the 606 | server should transpose the time to the timezone you have set in your 607 | calendar preferences. For some servers (like SOGo) this might work 608 | better than setting a "real" timezone. 609 | 610 | ** Troubleshooting 611 | 612 | If org-caldav reports a problem with the given URL, please 613 | triple-check that the URL is correct. It must point to a valid 614 | calendar on your CalDAV server. 615 | 616 | If the error is that the URL does not seem to accept DAV requests, you 617 | can additionally check with 'curl' by doing 618 | 619 | #+begin_src shell 620 | curl -D - -X OPTIONS --basic -u mylogin:mypassword URL 621 | #+end_src 622 | 623 | The output of this command must contain a 'DAV' header like this: 624 | 625 | #+begin_example 626 | DAV: 1, 3, extended-mkcol, access-control, ... etc. ... 627 | #+end_example 628 | 629 | By default, org-caldav will put all kinds of debug output into the 630 | buffer ~*org-caldav-debug*~. Look there if you're getting sync errors 631 | or if something plain doesn't work. If you're using an authinfo file 632 | and authentication doesn't work, set auth-info-debug to t and look in 633 | the ~*Messages*~ buffer. When you report a bug, please try to post the 634 | relevant portion of the ~*org-caldav-debug*~ buffer since it might be 635 | helpful to see what's going wrong. If Emacs throws an error, do 636 | 637 | #+begin_example 638 | M-x toggle-debug-on-error 639 | #+end_example 640 | 641 | and try to replicate the error to get a backtrace. 642 | 643 | You can also turn on excessive debugging by setting the variable 644 | ~org-caldav-debug-level~ to 2. This will also output the /contents/ of 645 | the events into the debug buffer. If you send such a buffer in a bug 646 | report, please make very sure you have removed personal information 647 | from those events. 648 | 649 | * Known Bugs 650 | 651 | - Syncing is currently pretty slow since everything is done 652 | synchronously. 653 | 654 | - Pretty much everything besides SUMMARY, DESCRIPTION, LOCATION and time 655 | is ignored in iCalendar. 656 | 657 | -------------------------------------------------------------------------------- /doc/org-caldav.texi: -------------------------------------------------------------------------------- 1 | \input texinfo @c -*- texinfo -*- 2 | @c %**start of header 3 | @setfilename org-caldav.info 4 | @settitle org-caldav 5 | @documentencoding UTF-8 6 | @documentlanguage en 7 | @c %**end of header 8 | 9 | @dircategory Emacs 10 | @direntry 11 | * Org-caldav: (org-caldav). Caldav sync for Emacs Orgmode. 12 | @end direntry 13 | 14 | @finalout 15 | @titlepage 16 | @title org-caldav 17 | @author Jack Kamm 18 | @end titlepage 19 | 20 | @contents 21 | 22 | @ifnottex 23 | @node Top 24 | @top org-caldav 25 | 26 | Caldav sync for Emacs Orgmode 27 | 28 | @end ifnottex 29 | 30 | @menu 31 | * Prerequisites:: 32 | * Basic setup:: 33 | * Usage:: 34 | * Advanced configuration:: 35 | * Compatible CalDav servers:: 36 | * Implementation details:: 37 | * Tips, Tricks, and Troubleshooting: Tips Tricks and Troubleshooting. 38 | * Known Bugs:: 39 | 40 | @detailmenu 41 | --- The Detailed Node Listing --- 42 | 43 | Basic setup 44 | 45 | * Minimal example:: 46 | * Required configuration settings:: 47 | 48 | Advanced configuration 49 | 50 | * Sync direction (one-way sync):: 51 | * Filtering entries:: 52 | * Syncing deletions:: 53 | * Storing authentication information in authinfo/netrc:: 54 | * Syncing with more than one calendar:: 55 | * Customizing the inbox:: 56 | * Syncing TODOs between Org and CalDav:: 57 | * Behavior of recurring TODO deadlines without a start time:: 58 | 59 | Compatible CalDav servers 60 | 61 | * Syncing to Google Calendar:: 62 | 63 | Implementation details 64 | 65 | * Org and the iCalendar format:: 66 | * Conflict handling:: 67 | * Repeating events and todos:: 68 | * How syncing happens (a.k.a. David's little CalDAV rant): How syncing happens (aka David's little CalDAV rant). 69 | 70 | Tips, Tricks, and Troubleshooting 71 | 72 | * Standalone import of ICS files:: 73 | * Storage of sync information and sync from different computers:: 74 | * Starting from scratch:: 75 | * Timezone problems:: 76 | * Troubleshooting:: 77 | 78 | @end detailmenu 79 | @end menu 80 | 81 | @node Prerequisites 82 | @chapter Prerequisites 83 | 84 | @itemize 85 | @item 86 | Emacs >= 26.3 87 | @item 88 | Org >= 9.1 89 | @item 90 | @ref{Compatible CalDav servers, , Compatible CalDav server} 91 | @end itemize 92 | 93 | @strong{IMPORTANT}: Before using this code, please make sure you have backups 94 | of your precious Org files. Also, it is recommended to create a new, 95 | empty calendar on your server for using this package. 96 | 97 | @strong{ALSO IMPORTANT}: When using this package, possibly all Org entries will 98 | get an UID property (see the docstring of @code{org-icalendar-store-UID} for 99 | further details). If you don't want this, then @emph{do not use this 100 | package}; there is just no way around that. It is the only reliable way 101 | to uniquely identify Org entries. 102 | 103 | @node Basic setup 104 | @chapter Basic setup 105 | 106 | At a minimum, set @code{org-caldav-url}, @code{org-caldav-calendar-id}, 107 | @code{org-caldav-inbox}, @code{org-caldav-files}, and @code{org-icalendar-timezone} 108 | (@ref{Required configuration settings}). 109 | 110 | @menu 111 | * Minimal example:: 112 | * Required configuration settings:: 113 | @end menu 114 | 115 | @node Minimal example 116 | @section Minimal example 117 | 118 | This example configures a single Org file @samp{/path/to/inbox.org} to sync 119 | with a single calendar (with ID @samp{CALENDAR-ID}) located on a Nextcloud 120 | server at @samp{NEXTCLOUD-SERVER-URL}. 121 | 122 | @lisp 123 | (require 'org-caldav) 124 | 125 | ;; URL of the caldav server 126 | (setq org-caldav-url "https://NEXTCLOUD-SERVER-URL/remote.php/dav/calendars/USERID") 127 | 128 | ;; calendar ID on server 129 | (setq org-caldav-calendar-id "CALENDAR-ID") 130 | 131 | ;; Org filename where new entries from calendar stored 132 | (setq org-caldav-inbox "/path/to/inbox.org") 133 | 134 | ;; Additional Org files to check for calendar events 135 | (setq org-caldav-files nil) 136 | 137 | ;; Usually a good idea to set the timezone manually 138 | (setq org-icalendar-timezone "Europe/Berlin") 139 | @end lisp 140 | 141 | @node Required configuration settings 142 | @section Required configuration settings 143 | 144 | @itemize 145 | @item 146 | Set @code{org-caldav-url} to the base address of your CalDAV server: 147 | 148 | @itemize 149 | @item 150 | Owncloud/Nextcloud (9.x and above): 151 | @uref{https://OWNCLOUD-SERVER-URL/remote.php/dav/calendars/USERID} 152 | @item 153 | Google: Set to symbol @code{'google}. See @ref{Syncing to Google Calendar} 154 | for additional required setup. 155 | @end itemize 156 | 157 | @item 158 | Set @code{org-caldav-calendar-id} to the calendar ID of your new calendar: 159 | 160 | @itemize 161 | @item 162 | Own/NextCloud: Click on that little symbol next to the calendar name 163 | and inspect the link of the calendar; the last element of the shown 164 | path is the calendar-id. This should @emph{usually} be the same as the 165 | name of the calendar, but not necessarily: Owncloud might replace 166 | certain characters (upper to lowercase, for instance), or it might 167 | even be entirely different if the calendar was created by another 168 | CalDAV application. 169 | @item 170 | Google: Click on 'calendar settings' and the id will be shown next 171 | to "Calendar Address". It is of the form 172 | @code{ID@@group.calendar.google.com}. Do @emph{not} omit the domain! 173 | @end itemize 174 | 175 | @item 176 | Set @code{org-caldav-inbox} to an org filename where new entries from the 177 | calendar should be stored. Just to be safe, it's a good idea to use 178 | an empty, dedicated Org file for that. 179 | 180 | @item 181 | Set @code{org-caldav-files} to the list of org files you would like to 182 | sync. The above @code{org-caldav-inbox} will be automatically added, so you 183 | don't have to add it here. 184 | 185 | @item 186 | It is usually a good idea to manually set @code{org-icalendar-timezone} to 187 | the timezone of your remote calendar. It should be a simple string 188 | like "Europe/Berlin". If that doesn't work and your events are 189 | shifted by a few hours, try the setting "UTC" (the SOGo calendar 190 | server seems to need this). 191 | @end itemize 192 | 193 | @node Usage 194 | @chapter Usage 195 | 196 | First, create a calendar on the CalDav server, and configure which Org 197 | files to sync it with (see @ref{Basic setup}). 198 | 199 | Then, do: 200 | 201 | @code{M-x org-caldav-sync} 202 | 203 | to sync between Org and CalDav. 204 | 205 | You will be prompted to manually enter the username/password on each 206 | sync; see @ref{Storing authentication information in authinfo/netrc, , Storing authentication information} on how to save the 207 | password and avoid manual entry. 208 | 209 | If you have many calendar items, the first sync can easily take 210 | several minutes, especially if using a slow CalDav implementation like 211 | Google's. If you have to abort the initial sync for some reason, just 212 | start @code{org-caldav-sync} again in the same Emacs session and you should 213 | get asked if you'd like to resume. Likewise for any errors -- 214 | especially when using Google Calendar, it is not unusual to get stuff 215 | like '409' errors during the initial sync. Just run @code{org-caldav-sync} 216 | again until all events are uploaded. 217 | 218 | @node Advanced configuration 219 | @chapter Advanced configuration 220 | 221 | Before reading this section, first consult the section on @ref{Basic setup} 222 | (and in particular @ref{Required configuration settings}). 223 | 224 | Additional, advanced configuration options are listed below in this 225 | section. 226 | 227 | Note that org-caldav uses @uref{https://orgmode.org/manual/iCalendar-Export.html, ox-icalendar.el} to export from Org to 228 | iCalendar, so it's worth checking the options there as well. For 229 | example, use @code{org-icalendar-alarm-time} to add a reminder to your 230 | entries. 231 | 232 | @menu 233 | * Sync direction (one-way sync):: 234 | * Filtering entries:: 235 | * Syncing deletions:: 236 | * Storing authentication information in authinfo/netrc:: 237 | * Syncing with more than one calendar:: 238 | * Customizing the inbox:: 239 | * Syncing TODOs between Org and CalDav:: 240 | * Behavior of recurring TODO deadlines without a start time:: 241 | @end menu 242 | 243 | @node Sync direction (one-way sync) 244 | @section Sync direction (one-way sync) 245 | 246 | By default, org-caldav does two-way syncing, that means it does not 247 | matter where and how you change an entry. You can also move Org 248 | entries freely from one file to another, as long as they are all 249 | listed in @code{org-caldav-files}. 250 | 251 | To do one-way sync only, set @code{org-caldav-sync-direction} to 252 | @code{'org->cal} or @code{'cal->org}, depending on which direction you'd like to 253 | have. If you choose @code{'org->cal}, then @code{org-caldav-inbox} won't matter 254 | and can be @code{nil}. Likewise, if you choose @code{'cal->org}, then 255 | @code{org-caldav-files} will be ignored and only the calendar will be 256 | imported into the inbox. 257 | 258 | WARNING: It is NOT safe to switch the same calendar between 1-way and 259 | 2-way sync modes. Doing so may cause unexpected behavior, such as 260 | deleting all events in the calendar. 261 | 262 | @node Filtering entries 263 | @section Filtering entries 264 | 265 | There are several possibilities to choose which entries should be 266 | synced and which not: 267 | 268 | @itemize 269 | @item 270 | If you only want to sync manually marked entries, use 271 | @code{org-caldav-select-tags}, which is directly mapped to 272 | @code{org-export-select-tags}, so see its doc-string on how it works. 273 | 274 | @item 275 | If you want to exclude certain tags, use @code{org-caldav-exclude-tags}, 276 | which is mapped to @code{org-icalendar-exclude} tags. 277 | 278 | @item 279 | If you want more fine grained control, use 280 | @code{org-caldav-skip-conditions}. The syntax of the conditions is 281 | described in the doc-string of @code{org-agenda-skip-if}. 282 | 283 | @item 284 | In case you just want to keep your remote calendar clean, set 285 | @code{org-caldav-days-in-past} to the number of days you want to keep in 286 | the past on the remote calendar. This does not affect your org files, 287 | it works just as a filter for entries older than N days. 288 | @end itemize 289 | 290 | Note however that the normal @code{org-agenda-skip-function(-global)} will 291 | @strong{not} have any effect on the icalendar exporter (this used to be the 292 | case, but changed with the new exporters). 293 | 294 | @node Syncing deletions 295 | @section Syncing deletions 296 | 297 | If you delete entries in your Org files, the corresponding iCalendar 298 | entries will by default get deleted. You can change that behavior with 299 | @code{org-caldav-delete-calendar-entries} to never delete, or to ask before 300 | deletion. 301 | 302 | You must be careful to not simply remove previously synced files from 303 | @code{org-caldav-files}, as org-caldav would view all the entries from those 304 | files as deleted and hence by default also delete them from the 305 | calendar. However, org-caldav should be able to detect this situation 306 | and warn you with the message 'Previously synced file(s) are missing', 307 | asking you whether to continue nonetheless. 308 | 309 | If you delete events in your calendar, you will by default get asked 310 | if you'd like to delete the corresponding Org event. You can change 311 | that behavior through @code{org-caldav-delete-org-entries}. 312 | 313 | If you answer a deletion request with "no", the event should get 314 | re-synced to the calendar next time you call @code{org-caldav-sync}. 315 | 316 | @node Storing authentication information in authinfo/netrc 317 | @section Storing authentication information in authinfo/netrc 318 | 319 | If you don't want to enter your user/password every time, you can 320 | store it permanently in an authinfo file. In Emacs, the auth-source 321 | package takes care of that, but the syntax for https authentication is 322 | a bit peculiar. You have to use a line like the following 323 | 324 | @example 325 | machine www.google.com:443 port https login username password secret 326 | @end example 327 | 328 | Note that you have to specify the port number in the URL and @emph{also} 329 | specify 'https' for the port. This is not a bug. For more information, 330 | see (info "auth"), especially section "Help for users". 331 | 332 | Since you are storing your password in a file you should encrypt it 333 | using GnuPG@. Emacs will prompt you for a decryption key when it tries 334 | to read the file. 335 | 336 | @node Syncing with more than one calendar 337 | @section Syncing with more than one calendar 338 | 339 | This can be done by setting the variable @code{org-caldav-calendars}. It 340 | should be a list of plists (a 'plist' is simply a list with alternating 341 | :key's and values). Through these plists, you can override the global 342 | values of variables like @code{org-caldav-calendar-id}, and calling 343 | @code{org-caldav-sync} will go through these plists in order. 344 | 345 | Example: 346 | 347 | @lisp 348 | (setq org-caldav-calendars 349 | '((:calendar-id "work@@whatever" :files ("~/org/work.org") 350 | :inbox "~/org/fromwork.org") 351 | (:calendar-id "stuff@@mystuff" 352 | :files ("~/org/sports.org" "~/org/play.org") 353 | :skip-conditions (regexp "soccer") 354 | :inbox "~/org/fromstuff.org")) ) 355 | @end lisp 356 | 357 | This means that you have two calendars with IDs "work@@whatever" and 358 | "stuff@@mystuff". Both will be accessed through the global value of 359 | org-caldav-url, since the key :url isn't specified. The calendar 360 | "work@@whatever" will be synced with the file 'work.org' and inbox 361 | 'fromwork.org', while "stuff@@mystuff" with 'sports.org' and 362 | 'play.org', @emph{unless} there's the string 'soccer' in the heading, and 363 | and inbox is 'fromstuff.org'. See the doc-string of 364 | @code{org-caldav-calendars} for more details on which keys you can use. 365 | 366 | @node Customizing the inbox 367 | @section Customizing the inbox 368 | 369 | See the doc-string of @code{org-caldav-inbox} if you want more flexibility in 370 | where new items should be put. Instead of simply providing a file, you 371 | can also choose an existing entry or headline, or put the entry under a 372 | datetree. 373 | 374 | @node Syncing TODOs between Org and CalDav 375 | @section Syncing TODOs between Org and CalDav 376 | 377 | This feature is relatively new and less well tested, so it is 378 | recommended to have backups before using it. It has been tested on 379 | nextcloud and radicale. 380 | 381 | To sync TODO's between Org and the CalDav server, do: 382 | 383 | @lisp 384 | (setq org-icalendar-include-todo 'all 385 | org-caldav-sync-todo t) 386 | @end lisp 387 | 388 | The first instructs the Org exporter to include TODOs; the second 389 | tells org-caldav to import icalendar VTODOs as Org TODOs. 390 | 391 | Other customizations to consider (see their documentation for more 392 | details): 393 | 394 | @itemize 395 | @item 396 | @code{org-caldav-todo-priority} to control how priority levels map between 397 | iCalendar and Org. 398 | @item 399 | @code{org-caldav-todo-percent-states} to convert between 400 | @code{org-todo-keywords} and iCalendar's percent-complete property. 401 | @item 402 | @code{org-caldav-todo-deadline-schedule-warning-days} to auto-create 403 | SCHEDULED timestamps when a DEADLINE is present (this might be useful 404 | for users of the OpenTasks app). 405 | @end itemize 406 | 407 | If you find that some Org entries get an extra tag which equals their 408 | CATEGORY, this might be caused by the CATEGORY being exported to 409 | iCalendar, and then re-imported to Org as a tag. In that case, do 410 | 411 | @lisp 412 | (setq org-icalendar-categories '(local-tags)) 413 | @end lisp 414 | 415 | to prevent the CATEGORY from being exported to iCalendar. This problem 416 | only seems to affect some CalDav servers: in particular, NextCloud 417 | is affected, but Radicale does not seem to experience this problem. 418 | 419 | @node Behavior of recurring TODO deadlines without a start time 420 | @section Behavior of recurring TODO deadlines without a start time 421 | 422 | Technically, the @uref{https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.4, iCalendar spec} requires repeating events and todos 423 | (i.e. having an @code{RRULE} property) to have a starting time (@code{DTSTART} 424 | in iCalendar, equivalent to @code{SCHEDULED} in Org TODOs). This means 425 | that, a TODO with a repeating @code{DEADLINE} but without a @code{SCHEDULED} 426 | property, such as below, is not allowed by the iCalendar spec: 427 | 428 | @example 429 | * TODO An example todo with a repeating deadline and no start time 430 | DEADLINE: <2024-09-15 Sun +1w> 431 | @end example 432 | 433 | This is a clear shortcoming of the iCalendar spec, because it @emph{does} 434 | allow tasks to have a standalone deadline without a starting time, but 435 | @emph{doesn't} allow such tasks to repeat. 436 | 437 | By default, @code{ox-icalendar} follows the iCalendar spec, and when 438 | exporting a TODO with a repeating @code{DEADLINE} but no @code{SCHEDULED} 439 | timestamp, will add a start time based on 440 | @code{org-deadline-warning-days}. On future syncs, this start time will be 441 | inserted into Org as a @code{SCHEDULED} timestamp. 442 | 443 | However, in practice, many iCalendar implementations ignore this 444 | limitation, and allow Todos with @code{DEADLINE} (@code{DUE}) times to have 445 | repeaters (@code{RRULE}), even if they are missing @code{SCHEDULED} (@code{DTSTART}) 446 | times. If your CalDav server allows this, then you may set the 447 | variable @code{org-icalendar-todo-unscheduled-start} to @code{nil}. This will 448 | prevent @code{ox-icalendar} from adding a start time to such TODOs, thus 449 | preventing the @code{SCHEDULED} timestamp from being inserted on future 450 | syncs. 451 | 452 | See @ref{Repeating events and todos} for more details about how org-caldav 453 | handles repeating events and todos. 454 | 455 | @node Compatible CalDav servers 456 | @chapter Compatible CalDav servers 457 | 458 | @itemize 459 | @item 460 | @strong{Owncloud} and @strong{Nextcloud}: Regularly tested. 461 | 462 | @item 463 | @strong{Radicale} and @strong{Baikal}: Works. If you get problems with 'Digest' 464 | authentication, switch back to 'Basic' (make sure to use https, 465 | though!). If you get asked for password repeatedly, put it in 466 | @code{.authinfo} file (@ref{Storing authentication information in authinfo/netrc, , Storing authentication information}). 467 | 468 | @item 469 | @strong{SOGo} and @strong{Kolab}: Reported to be working 470 | (@uref{https://docs.kolab.org/client-configuration/emacs.html}) 471 | 472 | @item 473 | @strong{Google Calendar}: Should work, but you need to register an 474 | application with the Google Developer Console for OAuth2 475 | authentication (see below), because Google explicitly forbids to put 476 | client id/secrets into open source software (see 477 | @uref{https://developers.google.com/terms}, section 4b, paragraph 1). Instead 478 | of doing that though, I'd rather suggest you choose another service 479 | provider. 480 | @end itemize 481 | 482 | @menu 483 | * Syncing to Google Calendar:: 484 | @end menu 485 | 486 | @node Syncing to Google Calendar 487 | @section Syncing to Google Calendar 488 | 489 | NOTE: Using org-caldav with Google Calendar may be currently 490 | broken. See @uref{https://github.com/dengste/org-caldav/issues/284} 491 | 492 | The CalDAV endpoint for Google Calendar requires OAuth2 493 | authentication. So first, you need to install the oauth2 library from 494 | GNU ELPA, and afterwards you need to acquire an application ID and 495 | secret from the Google Developer Console. For details on how to do 496 | this, follow the Google documentation at 497 | 498 | @uref{https://developers.google.com/google-apps/calendar/caldav/v2/guide#creating_your_client_id} 499 | 500 | Put the client ID and secret into @code{org-caldav-oauth2-client-id} and 501 | @code{org-caldav-oauth2-client-secret}, respectively. Then set 502 | @code{org-caldav-url} to the symbol @code{'google}, and look up the 503 | @code{org-caldav-calendar-id} as described above. 504 | 505 | On first connection, the oauth2 library should redirect you to the 506 | Google OAuth2 authentication site. This requires a javascript enabled 507 | browser, so make sure that @code{browse-url-browser-function} is set to 508 | something like @code{browse-url-firefox} (the internal eww or w3m browsers 509 | will @strong{not} work). After authentication, you will be given a key that 510 | you have to paste into the Emacs prompt. The oauth2 library will save 511 | this key in Emacs' secure plist store, which is encrypted with 512 | GnuPG@. If you have not yet used a secure plist store, you will be 513 | asked for its encryption passphrase. In the future, you should only 514 | need to enter that passphrase again to connect with Google Calendar. 515 | 516 | By default, plstore will @strong{not} cache your entered password, so it will 517 | possibly ask you @strong{many} times. To activate caching, use 518 | 519 | @lisp 520 | (setq plstore-cache-passphrase-for-symmetric-encryption t) 521 | @end lisp 522 | 523 | @node Implementation details 524 | @chapter Implementation details 525 | 526 | @menu 527 | * Org and the iCalendar format:: 528 | * Conflict handling:: 529 | * Repeating events and todos:: 530 | * How syncing happens (a.k.a. David's little CalDAV rant): How syncing happens (aka David's little CalDAV rant). 531 | @end menu 532 | 533 | @node Org and the iCalendar format 534 | @section Org and the iCalendar format 535 | 536 | An Org entry can store much more information than an iCalendar entry, 537 | so there is no one-to-one correspondence between the two formats which 538 | makes syncing a bit difficult. 539 | 540 | @itemize 541 | @item 542 | Org to iCalendar 543 | @end itemize 544 | 545 | This package uses the org-icalendar package to do the export to the 546 | iCalendar format (.ics files). By default, it uses the title of the 547 | Org entry as SUMMARY and puts the entry's body into DESCRIPTION, 548 | snipping stuff like properties and timestamps (you can override that 549 | with properties of the same name, but IMO it makes stuff just more 550 | complicated). The variable @code{org-icalendar-include-body} 551 | denotes how many characters from the body should be included as 552 | DESCRIPTION (by default all characters are included). 553 | 554 | @itemize 555 | @item 556 | iCalendar to Org 557 | @end itemize 558 | 559 | If you create a new iCalendar entry in your calendar, you'll get an 560 | Org entry with SUMMARY as heading, DESCRIPTION as body and the 561 | timestamp. However, if you @emph{change} an existing entry in the calendar, 562 | things get more complicated and the variable 563 | @code{org-caldav-sync-changes-to-org} comes into play. Its default is the 564 | symbol "title-and-timestamp", which means that only the entry's 565 | heading is synced (with SUMMARY) and the timestamp gets updated, but 566 | @emph{not} the entry's body with DESCRIPTION@. The simple reason is that 567 | you might loose data, since DESCRIPTION is rather limited in what it 568 | can store. Still, you can set the variable to the symbol "all", which 569 | will completely @emph{replace} an existing Org entry with the entry that 570 | gets generated from the calendar's event. You can also limit syncing 571 | to heading and/or timestamp only. 572 | 573 | To be extra safe, org-caldav will by default backup entries it 574 | changes. See the variable @code{org-caldav-backup-file} for details. 575 | 576 | @itemize 577 | @item 578 | Org sexp entries 579 | @end itemize 580 | 581 | A special case are sexp entries like 582 | 583 | @example 584 | %%(diary-anniversary 2 2 1969) Foo's birthday 585 | 586 | * Regular meeting 587 | <%%(diary-float t 4 2)> 588 | @end example 589 | 590 | As you can see, they can appear in two different ways: plain by 591 | themselves, or inside an Org entry. If they are inside an Org entry, 592 | there's a good chance they will be exported (see below) and have an ID 593 | property, so they can be found by org-caldav. We can sync the title, 594 | but syncing the timestamp with the s-expression is just infeasible, so 595 | this will generate a sync error (which are @emph{not} critical; you'll just 596 | see them at the end of the sync, just so that you're aware that some 597 | stuff wasn't synced properly). 598 | 599 | However, sexp-entries are insanely flexible, and there are limits as 600 | to what the icalendar exporter will handle. For example, this here 601 | 602 | @example 603 | ** Regular event 604 | <%%(memq (calendar-day-of-week date) '(1 3 5))> 605 | @end example 606 | 607 | will not be exported at all. 608 | 609 | If the sexp entry is not inside an Org entry but stands by itself, 610 | they still will be exported, but they won't get an ID (since IDs are 611 | properties linked to Org entries). In practice, that means that you 612 | can delete and change them inside Org and this will be synced, but if 613 | you @emph{change} them in the @emph{calendar}, this will @emph{not} get synced 614 | back. Org-caldav just cannot find those entries, so this will generate 615 | a one-time sync error instead (again: those are not critical, just 616 | FYI). If you don't want those entries to be exported at all, just set 617 | @code{org-icalendar-include-sexps} to nil. 618 | 619 | @node Conflict handling 620 | @section Conflict handling 621 | 622 | Now that's an easy one: Org always wins. That means, if you change an 623 | entry in Org @emph{and} in the calendar, the changes in the calendar will 624 | be lost. I might implement proper conflict handling some day, but 625 | don't hold your breath (patches are welcome, of course). 626 | 627 | @node Repeating events and todos 628 | @section Repeating events and todos 629 | 630 | Org-caldav has basic support for repeating events and todos. In 631 | particular, simple Org timestamp repeaters such as @code{+3d} or @code{+1m} can 632 | be succesfully sync'd bidirectionally. 633 | 634 | However, complex iCalender recurrences, such as "repeat on the 2nd 635 | Tuesday of each month until X date", are not supported. 636 | 637 | For Org TODOs with both @code{SCHEDULED} and @code{DEADLINE} timestamps, each of 638 | the timestamps must have the same repeater, otherwise the behavior is 639 | undefined. 640 | 641 | Furthermore, the behavior of Org TODOs with a repeating @code{DEADLINE} 642 | timestamp, but no @code{SCHEDULED} timestamp, has some subtleties; see the 643 | configuration section: @ref{Behavior of recurring TODO deadlines without a start time}. 644 | 645 | @node How syncing happens (aka David's little CalDAV rant) 646 | @section How syncing happens (a.k.a. David's little CalDAV rant) 647 | 648 | (This is probably not interesting, so you can skip this.) 649 | 650 | CalDAV is a mess. 651 | 652 | First off, it is based on WebDAV, which has its own fair share of 653 | problems. The main design flaw of CalDAV however, is that UID and 654 | resource name (the "filename", if you want) are two different 655 | things. I know that there are reasons for that (not everything has a 656 | UID, like timezones, and you can put several events in one resource), 657 | but this is typical over-engineering to allow some marginal use cases 658 | pretty much no one needs. Another problem is that you have to do 659 | additional round-trips to get Etag and sequence number, which makes 660 | CalDAV pretty slow. 661 | 662 | Org-caldav takes the easy route: it assumes that every resource 663 | contains one event, and that UID and resource name are identical. In 664 | fact, Google's CalDAV interface even enforces the latter. And while 665 | Owncloud does not enforce it, at least it just does it if you create 666 | items in its web interface. 667 | 668 | However, the CalDAV standard does not demand this, so I guess there 669 | are servers out there with which org-caldav does not work. Patches 670 | welcome. 671 | 672 | Now, all this would be bad enough if it weren't for the sloppy server 673 | implementations which make implementing a CalDAV client a living hell 674 | and led to several rewrites of the code. Especially Google, the 500 675 | pound gorilla in the room, doesn't really care much for CalDAV@. I 676 | guess they really like their own shiny REST-based calendar API better, 677 | and I can't blame them for that. 678 | 679 | @node Tips Tricks and Troubleshooting 680 | @chapter Tips, Tricks, and Troubleshooting 681 | 682 | @menu 683 | * Standalone import of ICS files:: 684 | * Storage of sync information and sync from different computers:: 685 | * Starting from scratch:: 686 | * Timezone problems:: 687 | * Troubleshooting:: 688 | @end menu 689 | 690 | @node Standalone import of ICS files 691 | @section Standalone import of ICS files 692 | 693 | org-caldav can also be used for standalone import of ICS files to Org. 694 | 695 | In particular, see @code{org-caldav-import-ics-buffer-to-org} to import 696 | iCalendar entries (e.g. from e-mail attachments) directly to your 697 | @code{org-caldav-inbox}. 698 | 699 | Also, see @code{org-caldav-convert-ics-to-datetree} to convert an iCalendar 700 | file into an Org datetree in a separate buffer (use 701 | @code{org-caldav-datetree-treetype} to control the style of datetree). 702 | 703 | @node Storage of sync information and sync from different computers 704 | @section Storage of sync information and sync from different computers 705 | 706 | The current sync state is stored in a file @code{org-caldav-SOMEID.el} in 707 | the @code{/.emacs.d directory. You can change the location through the 708 | variable ~org-caldav-save-directory}. SOMEID directly depends on the 709 | calendar id (it's a snipped MD5). 710 | 711 | If you sync your Org files across different machines and want to use 712 | org-caldav on all of them, don't forget to sync the org sync state, 713 | too. Probably your best bet is to set @code{org-caldav-save-directory} to the 714 | path you have your Org files in, so that it gets copied alongside with 715 | them. 716 | 717 | @node Starting from scratch 718 | @section Starting from scratch 719 | 720 | If your sync state somehow gets broken, you can make a clean slate by 721 | doing 722 | 723 | @example 724 | C-u M-x org-caldav-delete-everything 725 | @end example 726 | 727 | The function has to be called with a prefix so that you don't call it 728 | by accident. This will delete everything in the calendar along with 729 | the current sync state. You can then call @code{org-caldav-sync} afterwards 730 | and it will completely put all Org events into the now empty 731 | calendar. Needless to say, don't do that if you have new events in 732 | your calendar which are not synced yet@dots{} 733 | 734 | Deleting many events can be slow, though; in that case, just delete 735 | the calendar and re-create it, delete the sync state file in 736 | ~/.emacs.d and restart Emacs. 737 | 738 | @node Timezone problems 739 | @section Timezone problems 740 | 741 | Timezone handling is plain horrible, and it seems every CalDAV server 742 | does it slightly differently, also using non-standard headers like 743 | X-WR-TIMEZONE@. If you see items being shifted by a few hours, make 744 | really really sure you have properly set @code{org-icalendar-timezone}, and 745 | that your calendar is configured to use the same one. 746 | 747 | If it still does not work, you can try setting @code{org-icalendar-timezone} 748 | to the string "UTC". This will put all events using UTC times and the 749 | server should transpose the time to the timezone you have set in your 750 | calendar preferences. For some servers (like SOGo) this might work 751 | better than setting a "real" timezone. 752 | 753 | @node Troubleshooting 754 | @section Troubleshooting 755 | 756 | If org-caldav reports a problem with the given URL, please 757 | triple-check that the URL is correct. It must point to a valid 758 | calendar on your CalDAV server. 759 | 760 | If the error is that the URL does not seem to accept DAV requests, you 761 | can additionally check with 'curl' by doing 762 | 763 | @example 764 | curl -D - -X OPTIONS --basic -u mylogin:mypassword URL 765 | @end example 766 | 767 | The output of this command must contain a 'DAV' header like this: 768 | 769 | @example 770 | DAV: 1, 3, extended-mkcol, access-control, ... etc. ... 771 | @end example 772 | 773 | By default, org-caldav will put all kinds of debug output into the 774 | buffer @code{*org-caldav-debug*}. Look there if you're getting sync errors 775 | or if something plain doesn't work. If you're using an authinfo file 776 | and authentication doesn't work, set auth-info-debug to t and look in 777 | the @code{*Messages*} buffer. When you report a bug, please try to post the 778 | relevant portion of the @code{*org-caldav-debug*} buffer since it might be 779 | helpful to see what's going wrong. If Emacs throws an error, do 780 | 781 | @example 782 | M-x toggle-debug-on-error 783 | @end example 784 | 785 | and try to replicate the error to get a backtrace. 786 | 787 | You can also turn on excessive debugging by setting the variable 788 | @code{org-caldav-debug-level} to 2. This will also output the @emph{contents} of 789 | the events into the debug buffer. If you send such a buffer in a bug 790 | report, please make very sure you have removed personal information 791 | from those events. 792 | 793 | @node Known Bugs 794 | @chapter Known Bugs 795 | 796 | @itemize 797 | @item 798 | Syncing is currently pretty slow since everything is done 799 | synchronously. 800 | 801 | @item 802 | Pretty much everything besides SUMMARY, DESCRIPTION, LOCATION and time 803 | is ignored in iCalendar. 804 | @end itemize 805 | 806 | @bye 807 | -------------------------------------------------------------------------------- /org-caldav-tests.el: -------------------------------------------------------------------------------- 1 | ;; Test suite for org-caldav.el 2 | ;; Copyright, authorship, license: see org-caldav.el. 3 | 4 | ;; Run it from the org-caldav directory like this: 5 | ;; TZ="Europe/Berlin" emacs -Q -L . --eval '(setq org-caldav-url "CALDAV-URL")' -l org-caldav-testsuite.el -f ert 6 | ;; 7 | ;; On the server, there must already exist two calendars "test1" and "test2". 8 | ;; These will completely wiped by running this test! 9 | ;; 10 | ;; Hint: In case you need a test server, one lightweight option is: 11 | ;; docker run -v /path/to/data:/data tomsquest/docker-radicale 12 | ;; Then, you can create the test1 calendar from Thunderbird like so: 13 | ;; Thunderbird -> New Calendar -> Network -> Location: 14 | ;; http://localhost:5232/test/test1/ (the trailing slash is 15 | ;; important), with username "test" and blank password. Then add an 16 | ;; event from Thunderbird to make sure the calendar exists. 17 | 18 | (require 'ert) 19 | (require 'org) 20 | (require 'org-caldav) 21 | (require 'cl-lib) 22 | 23 | (when (org-caldav-use-oauth2) 24 | (org-caldav-check-oauth2 org-caldav-url) 25 | (org-caldav-retrieve-oauth2-token org-caldav-url)) 26 | 27 | (defvar org-caldav-test-calendar-names '("test1" "test2")) 28 | 29 | (setq org-caldav-debug-level (max 2 org-caldav-debug-level)) 30 | 31 | (setq org-caldav-delete-calendar-entries 'always) 32 | (setq org-caldav-backup-file nil) 33 | (setq org-caldav-test-preamble 34 | "BEGIN:VCALENDAR 35 | VERSION:2.0 36 | CALSCALE:GREGORIAN 37 | X-WR-TIMEZONE:Europe/Berlin 38 | X-WR-CALDESC: 39 | BEGIN:VTIMEZONE 40 | TZID:Europe/Berlin 41 | X-LIC-LOCATION:Europe/Berlin 42 | BEGIN:DAYLIGHT 43 | TZOFFSETFROM:+0100 44 | TZOFFSETTO:+0200 45 | TZNAME:CEST 46 | DTSTART:19700329T020000 47 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 48 | END:DAYLIGHT 49 | BEGIN:STANDARD 50 | TZOFFSETFROM:+0200 51 | TZOFFSETTO:+0100 52 | TZNAME:CET 53 | DTSTART:19701025T030000 54 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 55 | END:STANDARD 56 | END:VTIMEZONE 57 | ") 58 | 59 | ;; First test event in calendar 60 | (setq org-caldav-test-ics1 61 | "BEGIN:VEVENT 62 | DTSTART;VALUE=DATE:20121220 63 | DTEND;VALUE=DATE:20121221 64 | DTSTAMP:20121218T212132Z 65 | UID:orgcaldavtest@cal1 66 | CREATED:20121216T205929Z 67 | DESCRIPTION:A first test 68 | LAST-MODIFIED:20121218T212132Z 69 | LOCATION: 70 | SUMMARY:Test appointment Number 1 71 | END:VEVENT 72 | ") 73 | 74 | ;; How it should end up in Org 75 | (setq org-caldav-test-ics1-org 76 | "\\* Test appointment Number 1 77 | \\s-*:PROPERTIES: 78 | \\s-*:ID:\\s-*orgcaldavtest@cal1 79 | \\s-*:END: 80 | \\s-*<2012-12-20 Thu> 81 | \\s-*A first test") 82 | 83 | ;; Dto., second one 84 | (setq org-caldav-test-ics2 85 | "BEGIN:VEVENT 86 | DTSTART;TZID=Europe/Berlin:20121205T190000 87 | DTEND;TZID=Europe/Berlin:20121205T200000 88 | DTSTAMP:20121219T213352Z 89 | UID:orgcaldavtest-cal2 90 | CREATED:20121219T213352Z 91 | DESCRIPTION:A second test 92 | LAST-MODIFIED:20121219T213352Z 93 | LOCATION: 94 | SUMMARY:Test appointment Number 2 95 | END:VEVENT 96 | ") 97 | 98 | (setq org-caldav-test-ics2-org 99 | "\\* Test appointment Number 2 100 | \\s-*:PROPERTIES: 101 | \\s-*:ID:\\s-*orgcaldavtest-cal2 102 | \\s-*:END: 103 | \\s-*<2012-12-05 Wed 19:00-20:00> 104 | \\s-*A second test") 105 | 106 | ;; First test task in calendar 107 | (setq org-caldav-test-ics3 108 | "BEGIN:VTODO 109 | UID:orgcaldavtest@cal3 110 | DTSTAMP:20220828T161432Z 111 | DTSTART;VALUE=DATE:20121223 112 | SUMMARY:A test task from iCal 113 | DESCRIPTION:ical test task 1 114 | PRIORITY:0 115 | STATUS:NEEDS-ACTION 116 | END:VTODO 117 | ") 118 | 119 | (setq org-caldav-test-ics3-org 120 | "\\* TODO A test task from iCal 121 | \\s-*SCHEDULED: <2012-12-23 Sun> 122 | \\s-*:PROPERTIES: 123 | \\s-*:ID: orgcaldavtest@cal3 124 | \\s-*:END: 125 | \\s-*ical test task 1") 126 | 127 | ;; Second test task in calendar 128 | (setq org-caldav-test-ics4 129 | "BEGIN:VTODO 130 | UID:orgcaldavtest-cal4 131 | DTSTAMP:20220828T161432Z 132 | DTSTART;VALUE=DATE:20121219 133 | DUE;VALUE=DATE:20121223 134 | SUMMARY:Another test task from iCal 135 | DESCRIPTION:ical test task 2 136 | PRIORITY:5 137 | STATUS:NEEDS-ACTION 138 | END:VTODO 139 | ") 140 | 141 | (setq org-caldav-test-ics4-org 142 | "\\* TODO \\[#B\\] Another test task from iCal 143 | \\s-*DEADLINE: <2012-12-23 Sun> SCHEDULED: <2012-12-19 Wed> 144 | \\s-*:PROPERTIES: 145 | \\s-*:ID: orgcaldavtest-cal4 146 | \\s-*:END: 147 | \\s-*ical test task 2") 148 | 149 | ;; First test entry in Org which should end up in calendar 150 | (setq org-caldav-test-org1 151 | "* This is a test 152 | :PROPERTIES: 153 | :ID: orgcaldavtest@org1 154 | :END: 155 | <2012-12-23 Sun 20:00-21:00> 156 | Foo Bar Baz 157 | ") 158 | 159 | ;; Dto., second one 160 | (setq org-caldav-test-org2 161 | "* This is another test 162 | :PROPERTIES: 163 | :ID: orgcaldavtest-org2 164 | :END: 165 | <2012-12-19 Wed 19:00-21:00> 166 | Baz Bar Foo 167 | ") 168 | 169 | (setq org-caldav-test-org3 170 | "* This is a test with a tag :sometag: 171 | :PROPERTIES: 172 | :ID: orgcaldavtest-org3 173 | :END: 174 | <2012-12-20 Thu 19:00-21:00> 175 | moose 176 | ") 177 | 178 | ;; First test task in Org which should end up in calendar 179 | (setq org-caldav-test-org4 180 | "* TODO A test task from Org 181 | SCHEDULED: <2012-12-23 Sun> 182 | :PROPERTIES: 183 | :ID: orgcaldavtest@org4 184 | :END: 185 | Org task 1 186 | ") 187 | 188 | ;; Dto., second one 189 | (setq org-caldav-test-org5 190 | "* TODO [#B] Another test task from Org 191 | DEADLINE: <2012-12-23 Sun> SCHEDULED: <2012-12-19 Wed> 192 | :PROPERTIES: 193 | :ID: orgcaldavtest-org5 194 | :END: 195 | Org task 2 196 | ") 197 | 198 | ;; All events after sync. 199 | (setq org-caldav-test-allevents 200 | '("orgcaldavtest@org1" "orgcaldavtest-org2" "orgcaldavtest@cal1" "orgcaldavtest-cal2")) 201 | 202 | (setq org-caldav-test-alltodos 203 | '("orgcaldavtest@org4" "orgcaldavtest-org5" "orgcaldavtest@cal3" "orgcaldavtest-cal4")) 204 | 205 | 206 | (setq org-caldav-test-sync-result 207 | '(("test1" "orgcaldavtest@cal1" new-in-cal cal->org) 208 | ("test1" "orgcaldavtest-cal2" new-in-cal cal->org))) 209 | 210 | ;; Test files. 211 | (defun org-caldav-test-calendar-empty-p () 212 | "Check if calendar is empty." 213 | (let ((output (org-caldav-url-dav-get-properties 214 | (org-caldav-events-url) "resourcetype"))) 215 | (unless (eq (plist-get (cdar output) 'DAV:status) 200) 216 | (error "Could not query CalDAV URL %s: %s." (org-caldav-events-url) (prin1-to-string output))) 217 | (= (length output) 1))) 218 | 219 | (defun org-caldav-test-set-up () 220 | "Make a clean slate." 221 | (message "SET UP") 222 | (unless (org-caldav-test-calendar-empty-p) 223 | (dolist (cur (org-caldav-get-event-etag-list)) 224 | (message "Deleting %s" (car cur)) 225 | (org-caldav-delete-event (car cur)))) 226 | (should (org-caldav-test-calendar-empty-p))) 227 | 228 | (defun org-caldav-test-put-events () 229 | "Put initial events to calendar." 230 | (message "PUT") 231 | (let ((org-caldav-calendar-preamble org-caldav-test-preamble) 232 | events) 233 | (with-temp-buffer 234 | (insert org-caldav-test-ics1) 235 | (should (org-caldav-put-event (current-buffer))) 236 | (erase-buffer) 237 | (insert org-caldav-test-ics2) 238 | (should (org-caldav-put-event (current-buffer)))) 239 | (should 240 | (setq events 241 | (org-caldav-get-event-etag-list))) 242 | (should (assoc "orgcaldavtest@cal1" events)) 243 | (should (assoc "orgcaldavtest-cal2" events)))) 244 | 245 | (defun org-caldav-test-setup-temp-files () 246 | (let ((tmpdir (make-temp-file "org-caldav-test-" t))) 247 | (message "Using tempdir %s" tmpdir) 248 | (setq org-caldav-save-directory (expand-file-name "org-caldav-savedir" tmpdir) 249 | org-caldav-backup-file (expand-file-name "org-caldav-test-backup.org" tmpdir) 250 | org-caldav-test-orgfile (expand-file-name "test.org" tmpdir) 251 | org-caldav-test-second-orgfile (expand-file-name "test-second.org" tmpdir) 252 | org-caldav-test-inbox (expand-file-name "inbox.org" tmpdir) 253 | org-id-locations-file (expand-file-name "org-id-locations" tmpdir) 254 | org-id-locations nil 255 | org-id-files nil 256 | org-caldav-test-current-tempdir tmpdir)) 257 | (make-directory org-caldav-save-directory) 258 | (with-current-buffer (find-file-noselect org-caldav-test-inbox) 259 | (save-buffer))) 260 | 261 | (defun org-caldav-test-cleanup () 262 | (dolist (name '("test.org" "test-second.org" "inbox.org" "org-id-locations")) 263 | (let ((buf (get-buffer name))) 264 | (when buf 265 | (with-current-buffer buf 266 | (set-buffer-modified-p nil) 267 | (kill-buffer))))) 268 | (setq org-id-locations nil) 269 | (setq org-caldav-event-list nil) 270 | (when org-caldav-test-current-tempdir 271 | (delete-directory org-caldav-test-current-tempdir t) 272 | (setq org-caldav-test-current-tempdir nil))) 273 | 274 | ;; This is one, big, big test, since pretty much everything depends on 275 | ;; the current calendar/org state and I cannot easily split it. 276 | (ert-deftest org-caldav-01-sync-test () 277 | (message "Setting up temporary files") 278 | (org-caldav-test-setup-temp-files) 279 | (setq org-caldav-calendar-id (car org-caldav-test-calendar-names)) 280 | ;; Set up orgfile. 281 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 282 | (insert org-caldav-test-org1 "\n") 283 | (insert org-caldav-test-org2 "\n") 284 | (save-buffer)) 285 | ;; Set up data for org-caldav. 286 | (setq org-caldav-files (list org-caldav-test-orgfile)) 287 | (setq org-caldav-inbox org-caldav-test-inbox) 288 | (message "Cleaning up upstream calendars") 289 | (org-caldav-test-set-up) 290 | (message "Putting events") 291 | (org-caldav-test-put-events) 292 | (message "1st SYNC") 293 | ;; Do the sync. 294 | (org-caldav-sync) 295 | ;; Check result. 296 | (should (member `(,org-caldav-calendar-id "orgcaldavtest@cal1" new-in-cal cal->org) 297 | org-caldav-sync-result)) 298 | (should (member `(,org-caldav-calendar-id "orgcaldavtest-cal2" new-in-cal cal->org) 299 | org-caldav-sync-result)) 300 | (should (member `(,org-caldav-calendar-id "orgcaldavtest@org1" new-in-org org->cal) 301 | org-caldav-sync-result)) 302 | (should (member `(,org-caldav-calendar-id "orgcaldavtest-org2" new-in-org org->cal) 303 | org-caldav-sync-result)) 304 | ;; State file should exist now. 305 | (should (file-exists-p 306 | (org-caldav-sync-state-filename org-caldav-calendar-id))) 307 | (let ((calevents (org-caldav-get-event-etag-list))) 308 | (should (= (length calevents) (length org-caldav-test-allevents))) 309 | ;; Org events should be in cal. 310 | (dolist (cur org-caldav-test-allevents) 311 | (should (assoc cur calevents)))) 312 | ;; Cal events should be in Org. 313 | (with-current-buffer (find-file-noselect org-caldav-test-inbox) 314 | (goto-char (point-min)) 315 | (should (re-search-forward org-caldav-test-ics1-org nil t)) 316 | (goto-char (point-min)) 317 | (should (re-search-forward org-caldav-test-ics2-org nil t))) 318 | 319 | (message "2nd SYNC") 320 | ;; Sync again. 321 | (org-caldav-sync) 322 | ;; Nothing should have happened. 323 | (should-not org-caldav-sync-result) 324 | (message "Changing org events") 325 | ;; Now change events in Org 326 | (with-current-buffer (find-buffer-visiting org-caldav-test-orgfile) 327 | (goto-char (point-min)) 328 | (search-forward "This is another test") 329 | (replace-match "This is a changed test heading") 330 | (search-forward "<2012-12-19 Wed 19:00-21:00>") 331 | (replace-match "<2012-12-19 Thu 20:00-22:00>")) 332 | (with-current-buffer (find-buffer-visiting org-caldav-test-inbox) 333 | (goto-char (point-min)) 334 | (search-forward "Test appointment Number 2") 335 | (replace-match "Appointment number 2 was changed!") 336 | (search-forward "<2012-12-05 Wed 19:00-20:00>") 337 | (replace-match "<2012-12-04 Tue 18:00-19:00>")) 338 | 339 | ;; And sync... 340 | (message "3rd SYNC") 341 | (org-caldav-sync) 342 | (should (equal `((,org-caldav-calendar-id "orgcaldavtest-cal2" changed-in-org org->cal) 343 | (,org-caldav-calendar-id "orgcaldavtest-org2" changed-in-org org->cal)) 344 | org-caldav-sync-result)) 345 | 346 | ;; Check if those events correctly end up in calendar. 347 | (with-current-buffer (org-caldav-get-event "orgcaldavtest-cal2") 348 | (goto-char (point-min)) 349 | (save-excursion 350 | (should (search-forward "SUMMARY:Appointment number 2 was changed!"))) 351 | (save-excursion 352 | (should (re-search-forward "DTSTART.*:20121204T\\(170000Z\\|180000\\)" nil t))) 353 | (save-excursion 354 | (should (re-search-forward "DTEND.*:20121204T\\(180000Z\\|190000\\)" nil t)))) 355 | 356 | (with-current-buffer (org-caldav-get-event "orgcaldavtest-org2") 357 | (goto-char (point-min)) 358 | (save-excursion 359 | (should (search-forward "SUMMARY:This is a changed test heading"))) 360 | (save-excursion 361 | (should (re-search-forward "DTSTART.*:20121219T\\(190000Z\\|200000\\)" nil t))) 362 | (save-excursion 363 | (should (re-search-forward "DTEND.*:20121219T\\(210000Z\\|220000\\)" nil t)))) 364 | 365 | ;; Now change events in Cal 366 | (message "Changing events in calendar") 367 | (with-current-buffer (org-caldav-get-event "orgcaldavtest@cal1") 368 | (goto-char (point-min)) 369 | (save-excursion 370 | (search-forward "SUMMARY:Test appointment Number 1") 371 | (replace-match "SUMMARY:Changed test appointment Number 1")) 372 | (save-excursion 373 | (re-search-forward "DTSTART\\(;.*\\)?:\\(20121220\\)") 374 | (replace-match "20121212" nil nil nil 2)) 375 | (save-excursion 376 | (re-search-forward "DTEND\\(;.*\\)?:\\(20121221\\)") 377 | (replace-match "20121213" nil nil nil 2)) 378 | (save-excursion 379 | (when (re-search-forward "SEQUENCE:\\s-*\\([0-9]+\\)" nil t) 380 | (replace-match (number-to-string 381 | (1+ (string-to-number (match-string 1)))) 382 | nil t nil 1))) 383 | (message "PUTting first changed event") 384 | (should (org-caldav-save-resource 385 | (concat (org-caldav-events-url) (url-hexify-string "orgcaldavtest@cal1.ics")) 386 | (encode-coding-string (buffer-string) 'utf-8)))) 387 | 388 | (with-current-buffer (org-caldav-get-event "orgcaldavtest@org1") 389 | (goto-char (point-min)) 390 | (save-excursion 391 | (search-forward "SUMMARY:This is a test") 392 | (replace-match "SUMMARY:This is a changed test")) 393 | (save-excursion 394 | (if (re-search-forward "DTSTART\\(;.*\\)?:\\(20121223T190000Z\\)" nil t) 395 | (replace-match "20121213T180000Z" nil nil nil 2) 396 | (re-search-forward "DTSTART\\(;.*\\)?:\\(20121223T200000\\)") 397 | (replace-match "20121213T190000" nil nil nil 2))) 398 | (save-excursion 399 | (if (re-search-forward "DTEND\\(;.*\\)?:\\(20121223T200000Z\\)" nil t) 400 | (replace-match "20121213T190000Z" nil nil nil 2) 401 | (re-search-forward "DTEND\\(;.*\\)?:\\(20121223T210000\\)") 402 | (replace-match "20121213T200000" nil nil nil 2))) 403 | (save-excursion 404 | (when (re-search-forward "SEQUENCE:\\s-*\\([0-9]+\\)" nil t) 405 | (replace-match (number-to-string 406 | (1+ (string-to-number (match-string 1)))) 407 | nil t nil 1))) 408 | (message "PUTting second changed event") 409 | (should (org-caldav-save-resource 410 | (concat (org-caldav-events-url) "orgcaldavtest@org1.ics") 411 | (encode-coding-string (buffer-string) 'utf-8)))) 412 | 413 | ;; Aaaand sync! 414 | (message "4th SYNC") 415 | (org-caldav-sync) 416 | 417 | (should (equal `((,org-caldav-calendar-id "orgcaldavtest@cal1" changed-in-cal cal->org) 418 | (,org-caldav-calendar-id "orgcaldavtest@org1" changed-in-cal cal->org)) 419 | org-caldav-sync-result)) 420 | 421 | (with-current-buffer (find-file-noselect org-caldav-test-inbox) 422 | (goto-char (point-min)) 423 | (should (re-search-forward 424 | "* Changed test appointment Number 1 425 | \\s-*:PROPERTIES: 426 | \\s-*:ID:\\s-*orgcaldavtest@cal1 427 | \\s-*:END: 428 | \\s-*<2012-12-12 Wed> 429 | \\s-*A first test"))) 430 | 431 | (message "Deleting event in Org") 432 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 433 | (goto-char (point-min)) 434 | (should (re-search-forward 435 | "* This is a changed test 436 | \\s-*:PROPERTIES: 437 | \\s-*:ID:\\s-*orgcaldavtest@org1 438 | \\s-*:END: 439 | \\s-*<2012-12-13 Thu 19:00-20:00> 440 | \\s-*Foo Bar Baz")) 441 | ;; Delete this event in Org 442 | (replace-match "")) 443 | 444 | ;; Sync 445 | (message "6th SYNC") 446 | (org-caldav-sync) 447 | 448 | ;; Event should be deleted in calendar 449 | (let ((calevents (org-caldav-get-event-etag-list))) 450 | (should (= (length calevents) 3)) 451 | (should-not (assoc '"orgcaldavtest@org1" calevents))) 452 | (should 453 | (equal org-caldav-sync-result 454 | `((,org-caldav-calendar-id "orgcaldavtest@org1" deleted-in-org removed-from-cal)))) 455 | (should-not 456 | (assoc '"orgcaldavtest@org1" org-caldav-event-list)) 457 | 458 | ;; Delete event in calendar 459 | (message "Delete event in calendar") 460 | (should (org-caldav-delete-event "orgcaldavtest-org2")) 461 | ;; Sync one last time 462 | (message "7th SYNC") 463 | (let ((org-caldav-delete-org-entries 'always)) 464 | (org-caldav-sync)) 465 | 466 | (should 467 | (equal org-caldav-sync-result 468 | `((,org-caldav-calendar-id "orgcaldavtest-org2" deleted-in-cal removed-from-org)))) 469 | ;; There shouldn't be anything left in that buffer 470 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 471 | (goto-char (point-min)) 472 | (should-not (re-search-forward "[:alnum:]" nil t))) 473 | (should-not 474 | (assoc '"orgcaldavtest-org2" org-caldav-event-list)) 475 | 476 | (org-caldav-test-cleanup) 477 | 478 | ) 479 | 480 | (ert-deftest org-caldav-02-change-heading-test () 481 | (with-current-buffer (get-buffer-create "headingtest") 482 | (erase-buffer) 483 | (insert "* This is a test without timestamp in headline\n" 484 | " <2009-08-08 Sat 10:00>\n whatever\n foo\n bar\n") 485 | (insert "* This is a test <2009-08-08 Sat> \n" 486 | " whatever\n foo\n bar\n") 487 | (insert "* <2009-08-08 Sat 14:00> This is another test\n" 488 | " whatever\n foo\n bar\n") 489 | (org-mode) 490 | (goto-char (point-min)) 491 | (save-excursion 492 | (org-caldav-change-heading "first changed heading")) 493 | (should (looking-at "^\\* first changed heading$")) 494 | (search-forward "*" nil t 2) 495 | (beginning-of-line) 496 | (save-excursion 497 | (org-caldav-change-heading "second changed heading")) 498 | (should (looking-at "^\\* second changed heading <2009-08-08 Sat> $")) 499 | (search-forward "*" nil t 2) 500 | (beginning-of-line) 501 | (save-excursion 502 | (org-caldav-change-heading "third changed heading")) 503 | (should (looking-at "^\\* <2009-08-08 Sat 14:00> third changed heading\n")) 504 | )) 505 | 506 | (ert-deftest org-caldav-03-insert-org-entry () 507 | "Make sure that `org-caldav-insert-org-event-or-todo' works fine." 508 | (let ((entry '((start-d . "01 01 2015") 509 | (start-t . "19:00") 510 | (end-d ."01 01 2015") 511 | (end-t . "20:00") 512 | (summary . "The summary") 513 | (description . "The description") 514 | (location . "location"))) 515 | (org-caldav-select-tags "")) 516 | (cl-flet ((write-entry (uid level) 517 | (with-temp-buffer 518 | (org-mode) ; useful to set org-mode's 519 | ; internal variables 520 | (org-caldav-insert-org-event-or-todo 521 | (append entry `((uid . ,uid) 522 | (level . ,level)))) 523 | (buffer-string)))) 524 | (should (string-match (concat 525 | "\\*\\s-+The summary\n" 526 | "\\s-*:PROPERTIES:\n" 527 | "\\s-*:LOCATION: location\n" 528 | "\\s-*:END:\n" 529 | "\\s-*<2015-01-01 Thu 19:00-20:00>\n" 530 | "\\s-*The description\n") 531 | (write-entry nil nil))) 532 | (should (string-match (concat 533 | "\\*\\*\\s-+The summary\n" 534 | "\\s-*:PROPERTIES:\n" 535 | "\\s-*:LOCATION: location\n" 536 | "\\s-*:END:\n" 537 | "\\s-*<2015-01-01 Thu 19:00-20:00>\n" 538 | "\\s-*The description\n") 539 | (write-entry nil 2))) 540 | (should (string-match (concat "\\*\\s-+The summary\n" 541 | "\\s-*:PROPERTIES:\n" 542 | "\\s-*:ID:\\s-*1\n" 543 | "\\s-*:LOCATION: location\n" 544 | "\\s-*:END:\n" 545 | "\\s-*<2015-01-01 Thu 19:00-20:00>\n" 546 | "\\s-*The description\n") 547 | (write-entry "1" nil)))))) 548 | 549 | (ert-deftest org-caldav-04-multiple-calendars () 550 | (org-caldav-test-setup-temp-files) 551 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 552 | (insert org-caldav-test-org1) 553 | (save-buffer)) 554 | 555 | (with-current-buffer (find-file-noselect org-caldav-test-second-orgfile) 556 | (insert org-caldav-test-org2) 557 | (save-buffer)) 558 | 559 | ;; Delete calendar contents 560 | (let ((org-caldav-calendar-id (car org-caldav-test-calendar-names))) 561 | (org-caldav-test-set-up)) 562 | (let ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names))) 563 | (org-caldav-test-set-up)) 564 | 565 | (let 566 | ((org-caldav-calendars 567 | `((:calendar-id ,(car org-caldav-test-calendar-names) 568 | :url ,org-caldav-url 569 | :files (,org-caldav-test-orgfile) 570 | :inbox ,org-caldav-test-orgfile) 571 | (:calendar-id ,(nth 1 org-caldav-test-calendar-names) 572 | :url ,org-caldav-url 573 | :files (,org-caldav-test-second-orgfile) 574 | :inbox ,org-caldav-test-second-orgfile)))) 575 | (org-caldav-sync)) 576 | 577 | ;; Check that each calendar has one event 578 | (let 579 | ((org-caldav-calendar-id (car org-caldav-test-calendar-names))) 580 | (should (org-caldav-get-event "orgcaldavtest@org1")) 581 | (should-error (org-caldav-get-event "orgcaldavtest-org2"))) 582 | 583 | (let 584 | ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names))) 585 | (should-error (org-caldav-get-event "orgcaldavtest@org1")) 586 | (should (org-caldav-get-event "orgcaldavtest-org2"))) 587 | 588 | (org-caldav-test-cleanup) 589 | ) 590 | 591 | (ert-deftest org-caldav-05-multiple-calendars-agenda-skip-function () 592 | (org-caldav-test-setup-temp-files) 593 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 594 | (insert org-caldav-test-org1) 595 | (insert org-caldav-test-org2) 596 | (insert org-caldav-test-org3) 597 | (save-buffer)) 598 | 599 | ;; Delete calendar contents 600 | (let ((org-caldav-calendar-id (car org-caldav-test-calendar-names))) 601 | (org-caldav-test-set-up)) 602 | (let ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names))) 603 | (org-caldav-test-set-up)) 604 | (message "Starting sync") 605 | (let 606 | ((org-caldav-calendars 607 | `((:calendar-id ,(car org-caldav-test-calendar-names) 608 | :url ,org-caldav-url 609 | :skip-conditions (regexp ":sometag:") 610 | :files (,org-caldav-test-orgfile) 611 | :inbox ,org-caldav-test-orgfile) 612 | (:calendar-id ,(nth 1 org-caldav-test-calendar-names) 613 | :url ,org-caldav-url 614 | :skip-conditions (notregexp ":sometag:") 615 | :files (,org-caldav-test-orgfile) 616 | :inbox ,org-caldav-test-orgfile)))) 617 | (org-caldav-sync)) 618 | 619 | ;; Check that each calendar has one event 620 | (let 621 | ((org-caldav-calendar-id (car org-caldav-test-calendar-names))) 622 | (should (org-caldav-get-event "orgcaldavtest@org1")) 623 | (should (org-caldav-get-event "orgcaldavtest-org2")) 624 | (should-error (org-caldav-get-event "orgcaldavtest-org3"))) 625 | 626 | (let 627 | ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names))) 628 | (should (org-caldav-get-event "orgcaldavtest-org3")) 629 | (should-error (org-caldav-get-event "orgcaldavtest@org1")) 630 | (should-error (org-caldav-get-event "orgcaldavtest-org2"))) 631 | 632 | ;; Make sure org-agenda-skip-function-global is not set permanently 633 | (should-not org-agenda-skip-function-global) 634 | 635 | (org-caldav-test-cleanup) 636 | ) 637 | 638 | ;; Make sure setting org-caldav-files to 'nil' does not 639 | ;; do anything weird. 640 | (ert-deftest org-caldav-06-org-caldav-files-nil () 641 | (message "Setting up temporary files") 642 | (org-caldav-test-setup-temp-files) 643 | (setq org-caldav-calendar-id (car org-caldav-test-calendar-names)) 644 | ;; Set org-caldav-files to nil 645 | (setq org-caldav-files nil) 646 | (setq org-caldav-inbox org-caldav-test-inbox) 647 | (message "Setting up upstream calendar") 648 | (org-caldav-test-set-up) 649 | (message "Putting events") 650 | (org-caldav-test-put-events) 651 | (org-caldav-sync) 652 | ;; Events must still be in calendar 653 | (should (org-caldav-get-event "orgcaldavtest@cal1")) 654 | (should (org-caldav-get-event "orgcaldavtest-cal2")) 655 | ;; Sync result 656 | (should (or (equal org-caldav-test-sync-result 657 | org-caldav-sync-result) 658 | (equal (reverse org-caldav-test-sync-result) 659 | org-caldav-sync-result))) 660 | (org-caldav-test-cleanup)) 661 | 662 | ;; Check that we are able to detect when an Org file was removed from 663 | ;; org-caldav-files between syncs. 664 | (ert-deftest org-caldav-07-detect-removed-file () 665 | (message "Setting up temporary files") 666 | (org-caldav-test-setup-temp-files) 667 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 668 | (insert org-caldav-test-org1) 669 | (save-buffer)) 670 | (with-current-buffer (find-file-noselect org-caldav-test-second-orgfile) 671 | (insert org-caldav-test-org2) 672 | (save-buffer)) 673 | (setq org-caldav-calendar-id (car org-caldav-test-calendar-names)) 674 | ;; Set org-caldav-files to nil 675 | (setq org-caldav-files (list org-caldav-test-orgfile org-caldav-test-second-orgfile)) 676 | (setq org-caldav-inbox org-caldav-test-inbox) 677 | (message "Setting up upstream calendar") 678 | (org-caldav-test-set-up) 679 | (message "Putting events") 680 | (org-caldav-test-put-events) 681 | (message "1st sync") 682 | (org-caldav-sync) 683 | ;; Remove one of the files 684 | (setq org-caldav-files (list org-caldav-test-second-orgfile)) 685 | ;; Sync again, binding yes-or-no-p to our test 686 | (setq org-caldav-test-seen-prompt nil) 687 | (let (octest-seen-prompt) 688 | (cl-letf (((symbol-function 'yes-or-no-p) 689 | (lambda (prompt) 690 | (setq octest-seen-prompt prompt) nil))) 691 | (message "2nd sync") 692 | (should-error (org-caldav-sync)) 693 | (should (string-match "WARNING: Previously synced" 694 | octest-seen-prompt)))) 695 | (org-caldav-test-cleanup)) 696 | 697 | (ert-deftest org-caldav-test-multiline-location () 698 | (with-temp-buffer 699 | (org-mode) 700 | (insert org-caldav-test-org1) 701 | (goto-char (point-min)) 702 | (let ((orig-id (alist-get "ID" (org-entry-properties) nil nil #'string=))) 703 | (org-caldav-change-location "multi\nline") 704 | (let ((props (org-entry-properties))) 705 | (should (string= (alist-get "ID" props nil nil #'string=) orig-id)) 706 | (should (string-match-p "multi" (alist-get "LOCATION" props nil nil #'string=))) 707 | (should (string-match-p "line" (alist-get "LOCATION" props nil nil #'string=))))))) 708 | 709 | (ert-deftest org-caldav-08-test-setting-sync-direction () 710 | (org-caldav-test-setup-temp-files) 711 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 712 | (insert org-caldav-test-org1) 713 | (insert org-caldav-test-org2) 714 | (insert org-caldav-test-org3) 715 | (save-buffer)) 716 | 717 | ;; Delete calendar contents 718 | (let ((org-caldav-calendar-id (car org-caldav-test-calendar-names))) 719 | (org-caldav-test-set-up)) 720 | (let ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names))) 721 | (org-caldav-test-set-up)) 722 | (message "Starting sync") 723 | (let 724 | ((org-caldav-calendars 725 | ;; First only syncs Org to calendar 726 | `((:calendar-id ,(car org-caldav-test-calendar-names) 727 | :url ,org-caldav-url 728 | :files (,org-caldav-test-orgfile) 729 | :inbox nil ;; No inbox needed 730 | :sync-direction org->cal) 731 | ;; Second only syncs Calendar to Org inbox 732 | (:calendar-id ,(nth 1 org-caldav-test-calendar-names) 733 | :url ,org-caldav-url 734 | :files nil ;; No files needed 735 | :inbox ,org-caldav-test-inbox 736 | :sync-direction cal->org)))) 737 | (org-caldav-sync) 738 | 739 | ;; First calendar should sync everything 740 | (let 741 | ((org-caldav-calendar-id (car org-caldav-test-calendar-names))) 742 | (should (org-caldav-get-event "orgcaldavtest@org1")) 743 | (should (org-caldav-get-event "orgcaldavtest-org2")) 744 | (should (org-caldav-get-event "orgcaldavtest-org3"))) 745 | 746 | ;; Second calendar syncs nothing from org to cal 747 | (let 748 | ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names))) 749 | (should-error (org-caldav-get-event "orgcaldavtest-org3")) 750 | (should-error (org-caldav-get-event "orgcaldavtest@org1")) 751 | (should-error (org-caldav-get-event "orgcaldavtest-org2"))) 752 | 753 | ;; Put calendar events in both calendars 754 | (let ((org-caldav-calendar-id (car org-caldav-test-calendar-names))) 755 | (org-caldav-test-put-events)) 756 | (let ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names))) 757 | (org-caldav-test-put-events)) 758 | ;; Sync again 759 | (org-caldav-sync) 760 | 761 | ;; Events are still there in first 762 | (let 763 | ((org-caldav-calendar-id (car org-caldav-test-calendar-names))) 764 | (should (org-caldav-get-event "orgcaldavtest@org1")) 765 | (should (org-caldav-get-event "orgcaldavtest-org2")) 766 | (should (org-caldav-get-event "orgcaldavtest-org3"))) 767 | ;; Events still not there in second 768 | (let 769 | ((org-caldav-calendar-id (nth 1 org-caldav-test-calendar-names))) 770 | (should-error (org-caldav-get-event "orgcaldavtest-org3")) 771 | (should-error (org-caldav-get-event "orgcaldavtest@org1")) 772 | (should-error (org-caldav-get-event "orgcaldavtest-org2"))) 773 | ;; But second should have new events in inbox 774 | (with-current-buffer (find-file-noselect org-caldav-test-inbox) 775 | (goto-char (point-min)) 776 | (should (re-search-forward org-caldav-test-ics1-org nil t)) 777 | (goto-char (point-min)) 778 | (should (re-search-forward org-caldav-test-ics2-org nil t)))) 779 | 780 | (org-caldav-test-cleanup) 781 | ) 782 | 783 | ;; Based on org-caldav-01-sync-test, but modified for todos 784 | (ert-deftest org-caldav-09-sync-test-todo () 785 | (let ((org-caldav-sync-todo t) 786 | (org-icalendar-include-todo 'all)) 787 | (message "Setting up temporary files") 788 | (org-caldav-test-setup-temp-files) 789 | (setq org-caldav-calendar-id (car org-caldav-test-calendar-names)) 790 | ;; Set up data for org-caldav. 791 | (setq org-caldav-files (list org-caldav-test-orgfile)) 792 | (setq org-caldav-inbox org-caldav-test-inbox) 793 | 794 | (message "Cleaning up upstream calendars") 795 | (org-caldav-test-set-up) 796 | 797 | ;; Set up orgfile. 798 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 799 | (insert org-caldav-test-org4 "\n") 800 | (insert org-caldav-test-org5 "\n") 801 | (save-buffer)) 802 | 803 | (message "Putting events") 804 | (let ((org-caldav-calendar-preamble org-caldav-test-preamble) 805 | events) 806 | (with-temp-buffer 807 | (insert org-caldav-test-ics3) 808 | (should (org-caldav-put-event (current-buffer))) 809 | (erase-buffer) 810 | (insert org-caldav-test-ics4) 811 | (should (org-caldav-put-event (current-buffer)))) 812 | (let ((events (org-caldav-get-event-etag-list))) 813 | (should (assoc "orgcaldavtest@cal3" events)) 814 | (should (assoc "orgcaldavtest-cal4" events)))) 815 | 816 | (message "1st SYNC") 817 | ;; Do the sync. 818 | (org-caldav-sync) 819 | ;;;; Check result. 820 | (should (= (length (org-caldav-get-event-etag-list)) 4)) 821 | (should (member `(,org-caldav-calendar-id "orgcaldavtest@cal3" new-in-cal cal->org) 822 | org-caldav-sync-result)) 823 | (should (member `(,org-caldav-calendar-id "orgcaldavtest-cal4" new-in-cal cal->org) 824 | org-caldav-sync-result)) 825 | (should (member `(,org-caldav-calendar-id "orgcaldavtest@org4" new-in-org org->cal) 826 | org-caldav-sync-result)) 827 | (should (member `(,org-caldav-calendar-id "orgcaldavtest-org5" new-in-org org->cal) 828 | org-caldav-sync-result)) 829 | ;; State file should exist now. 830 | (should (file-exists-p 831 | (org-caldav-sync-state-filename org-caldav-calendar-id))) 832 | (let ((calevents (org-caldav-get-event-etag-list))) 833 | (should (= (length calevents) (length org-caldav-test-alltodos))) 834 | ;; Org events should be in cal. 835 | (dolist (cur org-caldav-test-alltodos) 836 | (should (assoc cur calevents)))) 837 | ;; Cal events should be in Org. 838 | (with-current-buffer (find-file-noselect org-caldav-test-inbox) 839 | (goto-char (point-min)) 840 | (should (re-search-forward org-caldav-test-ics3-org nil t)) 841 | (goto-char (point-min)) 842 | (should (re-search-forward org-caldav-test-ics4-org nil t))) 843 | 844 | (message "2nd SYNC") 845 | ;; Sync again. 846 | (org-caldav-sync) 847 | ;; Nothing should have happened. 848 | (should-not org-caldav-sync-result) 849 | 850 | (message "Changing org events") 851 | ;; Now change events in Org 852 | (with-current-buffer (find-buffer-visiting org-caldav-test-orgfile) 853 | (goto-char (point-min)) 854 | (search-forward "TODO A test task from Org") 855 | (replace-match "DONE Finished test task from Org") 856 | (search-forward "SCHEDULED:") 857 | (replace-match "CLOSED: [2012-12-24 Mon 00:00] SCHEDULED:" t)) 858 | (with-current-buffer (find-buffer-visiting org-caldav-test-inbox) 859 | (goto-char (point-min)) 860 | (search-forward "TODO [#B] Another test task from iCal") 861 | (replace-match "DONE [#C] Another test task from iCal was finished!") 862 | (search-forward "DEADLINE:") 863 | (replace-match "CLOSED: [2012-12-20 Thu 00:00] DEADLINE:" t)) 864 | 865 | ;; And sync... 866 | (message "3rd SYNC") 867 | (org-caldav-sync) 868 | (should (equal `((,org-caldav-calendar-id "orgcaldavtest-cal4" changed-in-org org->cal) 869 | (,org-caldav-calendar-id "orgcaldavtest@org4" changed-in-org org->cal)) 870 | org-caldav-sync-result)) 871 | 872 | ;; Check if those events correctly end up in calendar. 873 | (with-current-buffer (org-caldav-get-event "orgcaldavtest-cal4") 874 | (goto-char (point-min)) 875 | (save-excursion 876 | (should (search-forward "SUMMARY:Another test task from iCal was finished!"))) 877 | (save-excursion 878 | (should (search-forward "PRIORITY:9"))) 879 | (save-excursion 880 | (should (search-forward "STATUS:COMPLETED"))) 881 | (save-excursion 882 | (should (re-search-forward "COMPLETED.*:20121220T000000")))) 883 | 884 | (with-current-buffer (org-caldav-get-event "orgcaldavtest@org4") 885 | (goto-char (point-min)) 886 | (save-excursion 887 | (should (search-forward "SUMMARY:Finished test task from Org"))) 888 | (save-excursion 889 | (should (search-forward "PRIORITY:0"))) 890 | (save-excursion 891 | (should (search-forward "STATUS:COMPLETED"))) 892 | (save-excursion 893 | (should (re-search-forward "COMPLETED.*:20121224T000000")))) 894 | 895 | ;; Now change events in Cal 896 | (message "Changing events in calendar") 897 | (with-current-buffer (org-caldav-get-event "orgcaldavtest@cal3") 898 | (goto-char (point-min)) 899 | (save-excursion 900 | (search-forward "SUMMARY:A test task from iCal") 901 | (replace-match "SUMMARY:Changed A test task from iCal")) 902 | (save-excursion 903 | (re-search-forward "DTSTART\\(;.*\\)?:\\(20121223\\)") 904 | (replace-match "20121224" nil nil nil 2)) 905 | (save-excursion 906 | (when (re-search-forward "SEQUENCE:\\s-*\\([0-9]+\\)" nil t) 907 | (replace-match (number-to-string 908 | (1+ (string-to-number (match-string 1)))) 909 | nil t nil 1))) 910 | (message "PUTting first changed event") 911 | (should (org-caldav-save-resource 912 | (concat (org-caldav-events-url) (url-hexify-string "orgcaldavtest@cal3.ics")) 913 | (encode-coding-string (buffer-string) 'utf-8)))) 914 | 915 | (with-current-buffer (org-caldav-get-event "orgcaldavtest-org5") 916 | (goto-char (point-min)) 917 | (save-excursion 918 | (search-forward "SUMMARY:Another test task from Org") 919 | (replace-match "SUMMARY:Changed Another test task from Org")) 920 | (save-excursion 921 | (search-forward "STATUS:NEEDS-ACTION") 922 | (replace-match "STATUS:COMPLETED\nCOMPLETED:20121224T000000")) 923 | (save-excursion 924 | (search-forward "PERCENT-COMPLETE:0") 925 | (replace-match "PERCENT-COMPLETE:100")) 926 | (save-excursion 927 | (when (re-search-forward "SEQUENCE:\\s-*\\([0-9]+\\)" nil t) 928 | (replace-match (number-to-string 929 | (1+ (string-to-number (match-string 1)))) 930 | nil t nil 1))) 931 | (message "PUTting second changed event") 932 | (should (org-caldav-save-resource 933 | (concat (org-caldav-events-url) "orgcaldavtest-org5.ics") 934 | (encode-coding-string (buffer-string) 'utf-8)))) 935 | 936 | ;; Aaaand sync! 937 | (message "4th SYNC") 938 | (org-caldav-sync) 939 | 940 | (should (equal `((,org-caldav-calendar-id "orgcaldavtest@cal3" changed-in-cal cal->org) 941 | (,org-caldav-calendar-id "orgcaldavtest-org5" changed-in-cal cal->org)) 942 | org-caldav-sync-result)) 943 | 944 | (with-current-buffer (find-file-noselect org-caldav-test-inbox) 945 | (goto-char (point-min)) 946 | (should (re-search-forward 947 | "* TODO Changed A test task from iCal 948 | \\s-*SCHEDULED: <2012-12-24 Mon> 949 | \\s-*:PROPERTIES: 950 | \\s-*:ID:\\s-*orgcaldavtest@cal3 951 | \\s-*:END: 952 | \\s-*ical test task 1"))) 953 | 954 | (message "Changing description in icalendar") 955 | (with-current-buffer (org-caldav-get-event "orgcaldavtest@cal3") 956 | (goto-char (point-min)) 957 | (save-excursion 958 | (search-forward "DESCRIPTION:ical test task 1") 959 | (replace-match "DESCRIPTION:ical test task 1 modified")) 960 | 961 | (message "PUTting changed description") 962 | (should (org-caldav-save-resource 963 | (concat (org-caldav-events-url) (url-hexify-string "orgcaldavtest@cal3.ics")) 964 | (encode-coding-string (buffer-string) 'utf-8)))) 965 | 966 | (message "5th SYNC") 967 | (let ((org-caldav-sync-changes-to-org 'all)) 968 | (org-caldav-sync)) 969 | 970 | (should (equal `((,org-caldav-calendar-id "orgcaldavtest@cal3" 971 | changed-in-cal cal->org)) 972 | org-caldav-sync-result)) 973 | 974 | (with-current-buffer (find-file-noselect org-caldav-test-inbox) 975 | (goto-char (point-min)) 976 | (should (re-search-forward 977 | "* TODO Changed A test task from iCal 978 | \\s-*SCHEDULED: <2012-12-24 Mon> 979 | \\s-*:PROPERTIES: 980 | \\s-*:ID:\\s-*orgcaldavtest@cal3 981 | \\s-*:END: 982 | \\s-*ical test task 1 modified"))) 983 | 984 | (message "Deleting event in Org") 985 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 986 | (goto-char (point-min)) 987 | (message (buffer-string)) 988 | (should (search-forward 989 | "* DONE [#B] Changed Another test task from Org :test: 990 | CLOSED: [2012-12-24 Mon 00:00] DEADLINE: <2012-12-23 Sun> SCHEDULED: <2012-12-19 Wed> 991 | :PROPERTIES: 992 | :ID: orgcaldavtest-org5 993 | :END: 994 | Org task 2 995 | 996 | ")) 997 | ;; Delete this event in Org 998 | (replace-match "")) 999 | 1000 | ;; Sync 1001 | (message "6th SYNC") 1002 | (org-caldav-sync) 1003 | 1004 | ;; Event should be deleted in calendar 1005 | (let ((calevents (org-caldav-get-event-etag-list))) 1006 | (should (= (length calevents) 3)) 1007 | (should-not (assoc '"orgcaldavtest-org5" calevents))) 1008 | (should 1009 | (equal org-caldav-sync-result 1010 | `((,org-caldav-calendar-id "orgcaldavtest-org5" deleted-in-org removed-from-cal)))) 1011 | (should-not 1012 | (assoc '"orgcaldavtest-org5" org-caldav-event-list)) 1013 | 1014 | ;; Delete event in calendar 1015 | (message "Delete event in calendar") 1016 | (should (org-caldav-delete-event "orgcaldavtest@org4")) 1017 | ;; Sync one last time 1018 | (message "7th SYNC") 1019 | (let ((org-caldav-delete-org-entries 'always)) 1020 | (org-caldav-sync)) 1021 | 1022 | (should 1023 | (equal org-caldav-sync-result 1024 | `((,org-caldav-calendar-id "orgcaldavtest@org4" deleted-in-cal removed-from-org)))) 1025 | ;; There shouldn't be anything left in that buffer 1026 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 1027 | (goto-char (point-min)) 1028 | (should-not (re-search-forward "[:alnum:]" nil t))) 1029 | (should-not 1030 | (assoc '"orgcaldavtest@org4" org-caldav-event-list)) 1031 | 1032 | (org-caldav-test-cleanup))) 1033 | 1034 | (ert-deftest org-caldav-10-test-description-cleanup () 1035 | (let ((org-caldav-sync-todo t) 1036 | (org-icalendar-include-todo 'all)) 1037 | (message "Setting up temporary files") 1038 | (org-caldav-test-setup-temp-files) 1039 | (setq org-caldav-calendar-id (car org-caldav-test-calendar-names)) 1040 | ;; Set up data for org-caldav. 1041 | (setq org-caldav-files (list org-caldav-test-orgfile)) 1042 | (setq org-caldav-inbox org-caldav-test-inbox) 1043 | 1044 | (message "Cleaning up upstream calendars") 1045 | (org-caldav-test-set-up) 1046 | 1047 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 1048 | (insert "* TODO Test links preserved in description 1049 | :PROPERTIES: 1050 | :ID: org-caldav-10-test-links-preserved 1051 | :END: 1052 | 1053 | https://orgmode.org 1054 | 1055 | * Test timestamp is filtered from description 1056 | :PROPERTIES: 1057 | :ID: org-caldav-10-test-timestamp-filtered 1058 | :END: 1059 | <2023-01-06 Fri> 1060 | 1061 | * 2nd test for timestamp filtering 1062 | :PROPERTIES: 1063 | :ID: org-caldav-10-test-timestamp-filtered2 1064 | :END: 1065 | <2023-01-06 Fri 14:00>--<2023-01-06 Fri 15:00>") 1066 | (save-buffer)) 1067 | 1068 | (org-caldav-sync) 1069 | 1070 | (with-current-buffer (org-caldav-get-event 1071 | "org-caldav-10-test-links-preserved") 1072 | (goto-char (point-min)) 1073 | (save-excursion 1074 | (should (search-forward "DESCRIPTION:")))) 1075 | 1076 | (with-current-buffer (org-caldav-get-event 1077 | "org-caldav-10-test-timestamp-filtered") 1078 | (goto-char (point-min)) 1079 | (save-excursion 1080 | (should (not (re-search-forward 1081 | "DESCRIPTION:.*2023-01-06" nil t))))) 1082 | 1083 | (with-current-buffer (org-caldav-get-event 1084 | "org-caldav-10-test-timestamp-filtered2") 1085 | (goto-char (point-min)) 1086 | (save-excursion 1087 | (should (not (re-search-forward 1088 | "DESCRIPTION:.*2023-01-06" nil t)))))) 1089 | 1090 | (org-caldav-test-cleanup)) 1091 | 1092 | (ert-deftest org-caldav-11-test-sync-unsaved () 1093 | (org-caldav-test-setup-temp-files) 1094 | (setq org-caldav-files (list org-caldav-test-orgfile)) 1095 | (setq org-caldav-inbox org-caldav-test-inbox) 1096 | (setq org-caldav-calendar-id (car org-caldav-test-calendar-names)) 1097 | (org-caldav-test-set-up) 1098 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 1099 | ;; Make sure the file exists 1100 | (save-buffer) 1101 | ;; Add an event, but don't save it 1102 | (insert org-caldav-test-org2)) 1103 | (org-caldav-sync)) 1104 | 1105 | (ert-deftest org-caldav-12-test-doublesync-created-id () 1106 | (org-caldav-test-setup-temp-files) 1107 | (setq org-caldav-files (list org-caldav-test-orgfile)) 1108 | (setq org-caldav-inbox org-caldav-test-inbox) 1109 | (setq org-caldav-calendar-id (car org-caldav-test-calendar-names)) 1110 | (org-caldav-test-set-up) 1111 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 1112 | (insert "* This is a test without ID\n" 1113 | " <2009-08-08 Sat 10:00>\n whatever\n foo\n bar\n") 1114 | (save-buffer)) 1115 | ;; First sync creates an ID for the event 1116 | (org-caldav-sync) 1117 | ;; Test if second sync can find the ID we created. If not, the test 1118 | ;; will exit with org-caldav error "Could not find UID" 1119 | (org-caldav-sync)) 1120 | 1121 | (defun org-caldav-test-input-output-entry (input output) 1122 | "Helper function to test Org->Cal->Org preserves an entry. 1123 | Clear the Org files and iCalendar. Then add INPUT to 1124 | `org-caldav-test-inbox'. Then sync it to iCalendar. Then reset 1125 | the Org files and database, pull from iCalendar, and check that 1126 | the result matches the regular expression OUTPUT." 1127 | (message "Setting up temporary files") 1128 | (org-caldav-test-setup-temp-files) 1129 | (setq org-caldav-calendar-id (car org-caldav-test-calendar-names)) 1130 | ;; Set up data for org-caldav. 1131 | (setq org-caldav-files (list org-caldav-test-orgfile)) 1132 | (setq org-caldav-inbox org-caldav-test-inbox) 1133 | 1134 | (message "Cleaning up upstream calendars") 1135 | (org-caldav-test-set-up) 1136 | 1137 | ;; Set up orgfile. 1138 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 1139 | (insert input) 1140 | (save-buffer)) 1141 | 1142 | (message "Sync") 1143 | ;; Sync event to iCal 1144 | (org-caldav-sync) 1145 | 1146 | ;; Reset org-caldav sync state 1147 | (delete-file (org-caldav-sync-state-filename org-caldav-calendar-id)) 1148 | (setq org-caldav-event-list nil) 1149 | (setq org-caldav-sync-result nil) 1150 | ;; Also delete the event in org 1151 | ;;(delete-file org-caldav-test-orgfile) 1152 | (with-current-buffer (find-file-noselect org-caldav-test-orgfile) 1153 | (erase-buffer) 1154 | (save-buffer)) 1155 | 1156 | ;; Sync event back to inbox 1157 | (org-caldav-sync) 1158 | (with-current-buffer (find-file-noselect org-caldav-test-inbox) 1159 | (goto-char (point-min)) 1160 | (should (re-search-forward 1161 | output)))) 1162 | 1163 | (ert-deftest org-caldav-13a-test-simple-repeating-event () 1164 | (org-caldav-test-input-output-entry 1165 | "* Simple repeating event 1166 | :PROPERTIES: 1167 | :ID: test-repeating-event 1168 | :END: 1169 | <2024-05-25 Sat +7d>" 1170 | "* Simple repeating event 1171 | :PROPERTIES: 1172 | :ID:\\s-+test-repeating-event 1173 | :END: 1174 | <2024-05-25 Sat \\+7d>")) 1175 | 1176 | (ert-deftest org-caldav-13b-test-simple-repeating-todo-dtstart () 1177 | (let ((org-caldav-sync-todo t) 1178 | (org-icalendar-include-todo 'all)) 1179 | (org-caldav-test-input-output-entry 1180 | "* TODO Simple repeating scheduled todo 1181 | SCHEDULED: <2024-06-08 Sat +3d> 1182 | :PROPERTIES: 1183 | :ID: test-simple-repeating-todo-dtstart 1184 | :END:" 1185 | "* TODO Simple repeating scheduled todo 1186 | SCHEDULED: <2024-06-08 Sat \\+3d> 1187 | :PROPERTIES: 1188 | :ID:\\s-+test-simple-repeating-todo-dtstart 1189 | :END:"))) 1190 | 1191 | (ert-deftest org-caldav-13c-test-simple-repeating-todo-dtstart-due () 1192 | (let ((org-caldav-sync-todo t) 1193 | (org-icalendar-include-todo 'all)) 1194 | (org-caldav-test-input-output-entry 1195 | "* TODO Simple repeating scheduled todo with deadline 1196 | SCHEDULED: <2024-06-08 Sat +3d> DEADLINE: <2024-06-10 Mon +3d> 1197 | :PROPERTIES: 1198 | :ID: test-simple-repeating-todo-dtstart-due 1199 | :END:" 1200 | "* TODO Simple repeating scheduled todo with deadline 1201 | \\(SCHEDULED: <2024-06-08 Sat \\+3d> DEADLINE: <2024-06-10 Mon \\+3d>\\|DEADLINE: <2024-06-10 Mon \\+3d> SCHEDULED: <2024-06-08 Sat \\+3d>\\) 1202 | :PROPERTIES: 1203 | :ID:\\s-+test-simple-repeating-todo-dtstart-due 1204 | :END:"))) 1205 | --------------------------------------------------------------------------------