Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F120835196
jmap_notes.c
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
32 KB
Referenced Files
None
Subscribers
None
jmap_notes.c
View Options
/* jmap_notes.c -- Routines for handling JMAP notes
*
* Copyright (c) 1994-2020 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include
<config.h>
#ifdef HAVE_UNISTD_H
#include
<unistd.h>
#endif
#include
<ctype.h>
#include
<string.h>
#include
<syslog.h>
#include
<assert.h>
#include
<errno.h>
#include
"acl.h"
#include
"append.h"
#include
"http_jmap.h"
#include
"http_proxy.h"
#include
"jmap_mail.h"
#include
"jmap_util.h"
#include
"json_support.h"
#include
"proxy.h"
#include
"sync_support.h"
#include
"times.h"
#include
"user.h"
#include
"util.h"
/* generated headers are not necessarily in current directory */
#include
"imap/http_err.h"
#include
"imap/imap_err.h"
#define APPLE_NOTES_ID "com.apple.mail-note"
static
int
jmap_notes_get
(
jmap_req_t
*
req
);
static
int
jmap_notes_set
(
jmap_req_t
*
req
);
static
int
jmap_notes_changes
(
jmap_req_t
*
req
);
static
jmap_method_t
jmap_notes_methods_standard
[]
=
{
{
NULL
,
NULL
,
NULL
,
0
}
};
static
jmap_method_t
jmap_notes_methods_nonstandard
[]
=
{
{
"Notes/get"
,
JMAP_NOTES_EXTENSION
,
&
jmap_notes_get
,
/*flags*/
0
},
{
"Notes/set"
,
JMAP_NOTES_EXTENSION
,
&
jmap_notes_set
,
JMAP_READ_WRITE
},
{
"Notes/changes"
,
JMAP_NOTES_EXTENSION
,
&
jmap_notes_changes
,
/*flags*/
0
},
{
NULL
,
NULL
,
NULL
,
0
}
};
HIDDEN
void
jmap_notes_init
(
jmap_settings_t
*
settings
)
{
if
(
!
config_getstring
(
IMAPOPT_NOTESMAILBOX
))
return
;
jmap_method_t
*
mp
;
for
(
mp
=
jmap_notes_methods_standard
;
mp
->
name
;
mp
++
)
{
hash_insert
(
mp
->
name
,
mp
,
&
settings
->
methods
);
}
json_object_set_new
(
settings
->
server_capabilities
,
JMAP_NOTES_EXTENSION
,
json_object
());
if
(
config_getswitch
(
IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS
))
{
for
(
mp
=
jmap_notes_methods_nonstandard
;
mp
->
name
;
mp
++
)
{
hash_insert
(
mp
->
name
,
mp
,
&
settings
->
methods
);
}
}
}
HIDDEN
void
jmap_notes_capabilities
(
json_t
*
account_capabilities
)
{
if
(
!
config_getstring
(
IMAPOPT_NOTESMAILBOX
))
return
;
if
(
config_getswitch
(
IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS
))
{
json_object_set_new
(
account_capabilities
,
JMAP_NOTES_EXTENSION
,
json_object
());
}
}
static
int
lookup_notes_collection
(
const
char
*
accountid
,
mbentry_t
**
mbentry
)
{
mbname_t
*
mbname
;
const
char
*
notesname
;
int
r
;
/* Create notes mailbox name from the parsed path */
mbname
=
mbname_from_userid
(
accountid
);
mbname_push_boxes
(
mbname
,
config_getstring
(
IMAPOPT_NOTESMAILBOX
));
/* XXX - hack to allow @domain parts for non-domain-split users */
if
(
httpd_extradomain
)
{
/* not allowed to be cross domain */
if
(
mbname_localpart
(
mbname
)
&&
strcmpsafe
(
mbname_domain
(
mbname
),
httpd_extradomain
))
{
r
=
HTTP_NOT_FOUND
;
goto
done
;
}
mbname_set_domain
(
mbname
,
NULL
);
}
/* Locate the mailbox */
notesname
=
mbname_intname
(
mbname
);
r
=
proxy_mlookup
(
notesname
,
mbentry
,
NULL
,
NULL
);
if
(
r
==
IMAP_MAILBOX_NONEXISTENT
)
{
/* Find location of INBOX */
char
*
inboxname
=
mboxname_user_mbox
(
accountid
,
NULL
);
int
r1
=
proxy_mlookup
(
inboxname
,
mbentry
,
NULL
,
NULL
);
free
(
inboxname
);
if
(
r1
==
IMAP_MAILBOX_NONEXISTENT
)
{
r
=
IMAP_INVALID_USER
;
goto
done
;
}
int
rights
=
httpd_myrights
(
httpd_authstate
,
*
mbentry
);
if
((
rights
&
JACL_CREATECHILD
)
!=
JACL_CREATECHILD
)
{
r
=
IMAP_PERMISSION_DENIED
;
goto
done
;
}
if
(
*
mbentry
)
free
((
*
mbentry
)
->
name
);
else
*
mbentry
=
mboxlist_entry_create
();
(
*
mbentry
)
->
name
=
xstrdup
(
notesname
);
}
else
if
(
!
r
)
{
int
rights
=
httpd_myrights
(
httpd_authstate
,
*
mbentry
);
if
((
rights
&
JACL_ADDITEMS
)
!=
JACL_ADDITEMS
)
{
r
=
IMAP_PERMISSION_DENIED
;
goto
done
;
}
}
done
:
mbname_free
(
&
mbname
);
return
r
;
}
static
int
ensure_notes_collection
(
const
char
*
accountid
,
mbentry_t
**
mbentryp
)
{
mbentry_t
*
mbentry
=
NULL
;
/* notes collection */
int
r
=
lookup_notes_collection
(
accountid
,
&
mbentry
);
if
(
!
r
)
{
// happy path
if
(
mbentryp
)
*
mbentryp
=
mbentry
;
else
mboxlist_entry_free
(
&
mbentry
);
return
0
;
}
// otherwise, clean up ready for next attempt
mboxlist_entry_free
(
&
mbentry
);
struct
mboxlock
*
namespacelock
=
user_namespacelock
(
accountid
);
// did we lose the race?
r
=
lookup_notes_collection
(
accountid
,
&
mbentry
);
if
(
r
==
IMAP_MAILBOX_NONEXISTENT
)
{
if
(
!
mbentry
)
goto
done
;
else
if
(
mbentry
->
server
)
{
proxy_findserver
(
mbentry
->
server
,
&
http_protocol
,
httpd_userid
,
&
backend_cached
,
NULL
,
NULL
,
httpd_in
);
goto
done
;
}
r
=
mboxlist_createmailbox
(
mbentry
->
name
,
MBTYPE_EMAIL
,
NULL
,
1
/* admin */
,
accountid
,
httpd_authstate
,
0
,
0
,
0
,
0
,
NULL
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"IOERROR: failed to create %s (%s)"
,
mbentry
->
name
,
error_message
(
r
));
}
else
{
char
*
userid
=
mboxname_to_userid
(
mbentry
->
name
);
static
const
char
*
annot
=
"/specialuse"
;
struct
buf
buf
=
BUF_INITIALIZER
;
buf_init_ro_cstr
(
&
buf
,
"
\\
XNotes"
);
r
=
annotatemore_write
(
mbentry
->
name
,
annot
,
userid
,
&
buf
);
free
(
userid
);
buf_reset
(
&
buf
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"failed to write annotation %s: %s"
,
annot
,
error_message
(
r
));
goto
done
;
}
}
}
done
:
mboxname_release
(
&
namespacelock
);
if
(
mbentryp
&&
!
r
)
*
mbentryp
=
mbentry
;
else
mboxlist_entry_free
(
&
mbentry
);
return
r
;
}
struct
get_rock
{
struct
jmap_get
*
get
;
struct
buf
*
buf
;
};
static
int
_note_get
(
message_t
*
msg
,
json_t
*
note
,
hash_table
*
props
,
int
want_created
,
struct
buf
*
buf
)
{
int
r
;
/* created */
if
(
want_created
)
{
r
=
message_get_field
(
msg
,
"X-Mail-Created-Date"
,
MESSAGE_DECODED
|
MESSAGE_TRIM
,
buf
);
if
(
r
)
return
r
;
json_object_set_new
(
note
,
"created"
,
json_string
(
buf_cstring
(
buf
)));
}
/* isFlagged */
if
(
jmap_wantprop
(
props
,
"isFlagged"
))
{
uint32_t
system_flags
;
r
=
message_get_systemflags
(
msg
,
&
system_flags
);
if
(
r
)
return
r
;
json_object_set_new
(
note
,
"isFlagged"
,
json_boolean
(
system_flags
&
FLAG_FLAGGED
));
}
/* lastSaved */
if
(
jmap_wantprop
(
props
,
"lastSaved"
))
{
char
datestr
[
RFC3339_DATETIME_MAX
];
time_t
t
;
r
=
message_get_savedate
(
msg
,
&
t
);
if
(
r
)
return
r
;
time_to_rfc3339
(
t
,
datestr
,
RFC3339_DATETIME_MAX
);
json_object_set_new
(
note
,
"lastSaved"
,
json_string
(
datestr
));
}
/* title */
if
(
jmap_wantprop
(
props
,
"title"
))
{
buf_reset
(
buf
);
r
=
message_get_subject
(
msg
,
buf
);
if
(
r
)
return
r
;
json_object_set_new
(
note
,
"title"
,
json_string
(
buf_cstring
(
buf
)));
}
/* body */
if
(
jmap_wantprop
(
props
,
"body"
))
{
int
encoding
=
0
;
const
char
*
charset_id
=
NULL
;
charset_t
charset
=
CHARSET_UNKNOWN_CHARSET
;
buf_reset
(
buf
);
r
=
message_get_body
(
msg
,
buf
);
if
(
!
r
)
r
=
message_get_encoding
(
msg
,
&
encoding
);
if
(
!
r
)
r
=
message_get_charset_id
(
msg
,
&
charset_id
);
if
(
r
)
return
r
;
charset
=
charset_lookupname
(
charset_id
);
if
(
encoding
||
strcasecmp
(
charset_canon_name
(
charset
),
"utf-8"
))
{
char
*
dec
=
charset_to_utf8
(
buf_cstring
(
buf
),
buf_len
(
buf
),
charset
,
encoding
);
buf_setcstr
(
buf
,
dec
);
free
(
dec
);
}
charset_free
(
&
charset
);
json_object_set_new
(
note
,
"body"
,
json_string
(
buf_cstring
(
buf
)));
}
/* isHTML */
if
(
jmap_wantprop
(
props
,
"isHTML"
))
{
const
char
*
type
=
NULL
,
*
subtype
=
NULL
;
r
=
message_get_type
(
msg
,
&
type
);
if
(
!
r
)
r
=
message_get_subtype
(
msg
,
&
subtype
);
if
(
r
)
return
r
;
int
isHTML
=
!
strcasecmpsafe
(
"text"
,
type
)
&&
!
strcasecmpsafe
(
"html"
,
subtype
);
json_object_set_new
(
note
,
"isHTML"
,
json_boolean
(
isHTML
));
}
return
0
;
}
static
void
_notes_get_cb
(
const
char
*
id
,
message_t
*
msg
,
void
*
data
__attribute__
((
unused
)),
void
*
rock
)
{
struct
get_rock
*
grock
=
(
struct
get_rock
*
)
rock
;
struct
jmap_get
*
get
=
grock
->
get
;
json_t
*
note
=
json_pack
(
"{s:s}"
,
"id"
,
id
);
int
r
=
_note_get
(
msg
,
note
,
get
->
props
,
0
/*want_created*/
,
grock
->
buf
);
if
(
!
r
)
{
json_array_append_new
(
get
->
list
,
note
);
}
else
{
syslog
(
LOG_ERR
,
"jmap: Notes/get(%s): %s"
,
id
,
error_message
(
r
));
json_array_append_new
(
get
->
not_found
,
json_string
(
id
));
json_decref
(
note
);
}
}
static
void
foreach_note
(
struct
mailbox
*
mbox
,
hash_table
*
ids
,
void
(
*
proc
)(
const
char
*
,
message_t
*
,
void
*
,
void
*
),
void
*
rock
)
{
struct
mailbox_iter
*
iter
=
mailbox_iter_init
(
mbox
,
0
,
ITER_SKIP_EXPUNGED
);
struct
buf
buf
=
BUF_INITIALIZER
;
message_t
*
msg
;
while
((
msg
=
(
message_t
*
)
mailbox_iter_step
(
iter
)))
{
int
r
=
message_get_field
(
msg
,
"X-Uniform-Type-Identifier"
,
MESSAGE_DECODED
|
MESSAGE_TRIM
,
&
buf
);
if
(
r
||
strcmp
(
APPLE_NOTES_ID
,
buf_cstring
(
&
buf
)))
continue
;
r
=
message_get_field
(
msg
,
"X-Universally-Unique-Identifier"
,
MESSAGE_DECODED
|
MESSAGE_TRIM
,
&
buf
);
if
(
r
)
continue
;
void
*
data
=
NULL
;
const
char
*
id
=
buf_cstring
(
&
buf
);
if
(
ids
->
size
)
{
/* Do we have this id? */
data
=
hash_lookup
(
id
,
ids
);
if
(
!
data
)
{
/* Not in our list to act on */
continue
;
}
hash_del
(
id
,
ids
);
}
proc
(
id
,
msg
,
data
,
rock
);
}
mailbox_iter_done
(
&
iter
);
buf_free
(
&
buf
);
}
static
void
not_found_cb
(
const
char
*
id
,
void
*
data
__attribute__
((
unused
)),
void
*
rock
)
{
json_t
*
json
=
(
json_t
*
)
rock
;
if
(
json_is_object
(
json
))
{
json_t
*
err
=
json_pack
(
"{s:s}"
,
"type"
,
"notFound"
);
json_object_set_new
(
json
,
id
,
err
);
}
else
{
json_array_append_new
(
json
,
json_string
(
id
));
}
}
static
const
jmap_property_t
notes_props
[]
=
{
{
"id"
,
NULL
,
JMAP_PROP_SERVER_SET
|
JMAP_PROP_IMMUTABLE
|
JMAP_PROP_ALWAYS_GET
},
{
"isFlagged"
,
NULL
,
0
},
{
"lastSaved"
,
NULL
,
JMAP_PROP_SERVER_SET
},
{
"title"
,
NULL
,
0
},
{
"body"
,
NULL
,
0
},
{
"isHTML"
,
NULL
,
0
},
{
NULL
,
NULL
,
0
}
};
static
int
jmap_notes_get
(
jmap_req_t
*
req
)
{
struct
jmap_parser
parser
=
JMAP_PARSER_INITIALIZER
;
struct
jmap_get
get
;
json_t
*
err
=
NULL
;
mbentry_t
*
mbentry
=
NULL
;
struct
mailbox
*
mbox
=
NULL
;
hash_table
ids
=
HASH_TABLE_INITIALIZER
;
struct
buf
buf
=
BUF_INITIALIZER
;
int
rights
;
jmap_get_parse
(
req
,
&
parser
,
notes_props
,
/*allow_null_ids*/
1
,
NULL
,
NULL
,
&
get
,
&
err
);
if
(
err
)
{
jmap_error
(
req
,
err
);
goto
done
;
}
int
r
=
ensure_notes_collection
(
req
->
accountid
,
&
mbentry
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"jmap_notes_get: ensure_notes_collection(%s): %s"
,
req
->
accountid
,
error_message
(
r
));
goto
done
;
}
rights
=
jmap_myrights_mbentry
(
req
,
mbentry
);
r
=
jmap_openmbox
(
req
,
mbentry
->
name
,
&
mbox
,
0
);
mboxlist_entry_free
(
&
mbentry
);
if
(
r
)
goto
done
;
/* Does the client request specific notes? */
if
(
JNOTNULL
(
get
.
ids
))
{
size_t
i
;
json_t
*
val
;
construct_hash_table
(
&
ids
,
32
,
0
);
json_array_foreach
(
get
.
ids
,
i
,
val
)
{
hash_insert
(
json_string_value
(
val
),
(
void
*
)
1
,
&
ids
);
}
}
if
((
rights
&
JACL_READITEMS
)
==
JACL_READITEMS
)
{
struct
get_rock
grock
=
{
&
get
,
&
buf
};
foreach_note
(
mbox
,
&
ids
,
&
_notes_get_cb
,
&
grock
);
}
/* Any remaining ids are not found */
hash_enumerate
(
&
ids
,
&
not_found_cb
,
get
.
not_found
);
free_hash_table
(
&
ids
,
NULL
);
/* Build response */
buf_reset
(
&
buf
);
buf_printf
(
&
buf
,
MODSEQ_FMT
,
mbox
->
i
.
highestmodseq
);
get
.
state
=
buf_release
(
&
buf
);
jmap_ok
(
req
,
jmap_get_reply
(
&
get
));
jmap_closembox
(
req
,
&
mbox
);
done
:
jmap_parser_fini
(
&
parser
);
jmap_get_fini
(
&
get
);
return
0
;
}
static
int
_notes_setargs_check
(
const
char
*
id
,
json_t
*
args
,
json_t
**
err
)
{
struct
jmap_parser
parser
=
JMAP_PARSER_INITIALIZER
;
int
is_create
=
!
id
;
json_t
*
arg
;
int
r
=
0
;
/* Reject read-only properties */
if
(
json_object_get
(
args
,
"lastSaved"
))
{
jmap_parser_invalid
(
&
parser
,
"lastSaved"
);
}
arg
=
json_object_get
(
args
,
"id"
);
if
(
arg
&&
(
is_create
||
strcmpnull
(
id
,
json_string_value
(
arg
))))
{
jmap_parser_invalid
(
&
parser
,
"id"
);
}
/* Type-check other properties */
arg
=
json_object_get
(
args
,
"isFlagged"
);
if
(
arg
&&
!
json_is_boolean
(
arg
))
{
jmap_parser_invalid
(
&
parser
,
"isFlagged"
);
}
arg
=
json_object_get
(
args
,
"title"
);
if
(
arg
&&
!
json_is_string
(
arg
))
{
jmap_parser_invalid
(
&
parser
,
"title"
);
}
arg
=
json_object_get
(
args
,
"body"
);
if
(
arg
&&
(
!
json_is_string
(
arg
)
||
!
json_object_get
(
args
,
"isHTML"
)))
{
jmap_parser_invalid
(
&
parser
,
"body"
);
}
arg
=
json_object_get
(
args
,
"isHTML"
);
if
(
arg
&&
(
!
json_is_boolean
(
arg
)
||
!
json_object_get
(
args
,
"body"
)))
{
jmap_parser_invalid
(
&
parser
,
"isHTML"
);
}
if
(
json_array_size
(
parser
.
invalid
))
{
*
err
=
json_pack
(
"{s:s}"
,
"type"
,
"invalidProperties"
);
json_object_set
(
*
err
,
"properties"
,
parser
.
invalid
);
r
=
-1
;
}
jmap_parser_fini
(
&
parser
);
return
r
;
}
static
int
_note_create
(
struct
mailbox
*
mailbox
,
json_t
*
note
,
json_t
**
new_note
)
{
struct
stagemsg
*
stage
=
NULL
;
struct
appendstate
as
;
strarray_t
flags
=
STRARRAY_INITIALIZER
;
struct
buf
buf
=
BUF_INITIALIZER
;
struct
body
*
bodypart
=
NULL
;
char
datestr
[
80
],
*
from
,
*
title
,
*
body
;
FILE
*
f
=
NULL
;
int
r
=
0
,
isFlagged
=
0
,
isHTML
=
0
,
qpencode
=
0
;
time_t
now
=
time
(
0
);
json_t
*
prop
;
const
char
*
uid
=
NULL
,
*
created
=
NULL
;
/* Prepare to stage the message */
if
(
!
(
f
=
append_newstage
(
mailbox
->
name
,
now
,
0
,
&
stage
)))
{
syslog
(
LOG_ERR
,
"append_newstage(%s) failed"
,
mailbox
->
name
);
r
=
IMAP_IOERROR
;
goto
done
;
}
*
new_note
=
json_object
();
prop
=
json_object_get
(
note
,
"id"
);
if
(
prop
)
uid
=
json_string_value
(
prop
);
else
{
uid
=
makeuuid
();
json_object_set_new
(
*
new_note
,
"id"
,
json_string
(
uid
));
}
time_to_rfc3339
(
now
,
datestr
,
sizeof
(
datestr
));
json_object_set_new
(
*
new_note
,
"lastSaved"
,
json_string
(
datestr
));
time_to_rfc5322
(
now
,
datestr
,
sizeof
(
datestr
));
prop
=
json_object_get
(
note
,
"created"
);
if
(
prop
)
created
=
json_string_value
(
prop
);
else
created
=
datestr
;
prop
=
json_object_get
(
note
,
"title"
);
if
(
prop
)
{
buf_init_ro_cstr
(
&
buf
,
json_string_value
(
prop
));
title
=
charset_encode_mimeheader
(
buf_base
(
&
buf
),
buf_len
(
&
buf
),
0
);
}
else
{
title
=
xstrdup
(
""
);
json_object_set_new
(
*
new_note
,
"title"
,
json_string
(
title
));
}
prop
=
json_object_get
(
note
,
"body"
);
if
(
prop
)
{
buf_init_ro_cstr
(
&
buf
,
json_string_value
(
prop
));
const
char
*
cp
;
for
(
cp
=
buf_base
(
&
buf
);
*
cp
;
cp
++
)
{
if
(
*
cp
&
0x80
)
{
qpencode
=
1
;
break
;
}
}
if
(
qpencode
)
{
body
=
charset_qpencode_mimebody
(
buf_base
(
&
buf
),
buf_len
(
&
buf
),
0
,
NULL
);
}
else
body
=
buf_release
(
&
buf
);
}
else
{
body
=
xstrdup
(
""
);
json_object_set_new
(
*
new_note
,
"body"
,
json_string
(
body
));
}
prop
=
json_object_get
(
note
,
"isHTML"
);
if
(
prop
)
isHTML
=
json_boolean_value
(
prop
);
else
json_object_set_new
(
*
new_note
,
"isHTML"
,
json_false
());
prop
=
json_object_get
(
note
,
"isFlagged"
);
if
(
prop
)
isFlagged
=
json_boolean_value
(
prop
);
else
json_object_set_new
(
*
new_note
,
"isFlagged"
,
json_false
());
buf_reset
(
&
buf
);
if
(
strchr
(
httpd_userid
,
'@'
))
{
/* XXX This needs to be done via an LDAP/DB lookup */
buf_printf
(
&
buf
,
"<%s>"
,
httpd_userid
);
}
else
{
buf_printf
(
&
buf
,
"<%s@%s>"
,
httpd_userid
,
config_servername
);
}
from
=
charset_encode_mimeheader
(
buf_cstring
(
&
buf
),
buf_len
(
&
buf
),
0
);
fprintf
(
f
,
"MIME-Version: 1.0 (Cyrus-JMAP/%s)
\r\n
"
"X-Uniform-Type-Identifier: %s
\r\n
"
"X-Universally-Unique-Identifier: %s
\r\n
"
"X-Mail-Created-Date: %s
\r\n
"
"Date: %s
\r\n
"
"From: %s
\r\n
"
"Subject: %s
\r\n
"
"Content-Type: text/%s; charset=utf-8
\r\n
"
"Content-Transfer-Encoding: %s
\r\n\r\n
"
"%s"
,
CYRUS_VERSION
,
APPLE_NOTES_ID
,
uid
,
created
,
datestr
,
from
,
title
,
isHTML
?
"html"
:
"plain"
,
qpencode
?
"quoted-printable"
:
"7-bit"
,
body
);
free
(
title
);
free
(
from
);
free
(
body
);
if
(
ferror
(
f
)
||
fflush
(
f
))
{
r
=
IMAP_IOERROR
;
}
fclose
(
f
);
if
(
r
)
goto
done
;
/* Prepare to append the message to the mailbox */
r
=
append_setup_mbox
(
&
as
,
mailbox
,
httpd_userid
,
httpd_authstate
,
0
,
/*quota*/
NULL
,
0
,
0
,
/*event*/
0
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"append_setup(%s) failed: %s"
,
mailbox
->
name
,
error_message
(
r
));
goto
done
;
}
/* Append the message to the mailbox */
if
(
isFlagged
)
strarray_append
(
&
flags
,
"
\\
Flagged"
);
r
=
append_fromstage
(
&
as
,
&
bodypart
,
stage
,
now
,
0
,
&
flags
,
0
,
/*annots*/
NULL
);
if
(
r
)
{
append_abort
(
&
as
);
syslog
(
LOG_ERR
,
"append_fromstage(%s) failed: %s"
,
mailbox
->
name
,
error_message
(
r
));
goto
done
;
}
r
=
append_commit
(
&
as
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"append_commit(%s) failed: %s"
,
mailbox
->
name
,
error_message
(
r
));
goto
done
;
}
done
:
buf_free
(
&
buf
);
if
(
bodypart
)
{
message_free_body
(
bodypart
);
free
(
bodypart
);
}
strarray_fini
(
&
flags
);
append_removestage
(
stage
);
if
(
mailbox
)
{
if
(
r
)
mailbox_abort
(
mailbox
);
else
r
=
mailbox_commit
(
mailbox
);
}
if
(
r
)
{
json_decref
(
*
new_note
);
*
new_note
=
NULL
;
}
return
r
;
}
struct
set_rock
{
struct
jmap_set
*
set
;
struct
buf
*
buf
;
int
rights
;
};
static
void
_notes_update_cb
(
const
char
*
id
,
message_t
*
msg
,
void
*
data
,
void
*
rock
)
{
json_t
*
patch
=
(
json_t
*
)
data
;
struct
set_rock
*
srock
=
(
struct
set_rock
*
)
rock
;
struct
jmap_set
*
set
=
srock
->
set
;
json_t
*
err
=
NULL
,
*
updated_note
=
NULL
;
int
r
;
if
((
srock
->
rights
&
JACL_UPDATEITEMS
)
!=
JACL_UPDATEITEMS
)
{
int
read_only
=
!
(
srock
->
rights
&
JACL_READITEMS
);
err
=
json_pack
(
"{s:s}"
,
"type"
,
read_only
?
"notFound"
:
"forbidden"
);
}
else
if
(
!
_notes_setargs_check
(
id
,
patch
,
&
err
))
{
json_t
*
note
=
json_pack
(
"{s:s}"
,
"id"
,
id
);
r
=
_note_get
(
msg
,
note
,
NULL
,
1
/*want_created*/
,
srock
->
buf
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"jmap: Notes/update(%s) fetch: %s"
,
id
,
error_message
(
r
));
}
else
{
json_t
*
new_note
=
jmap_patchobject_apply
(
note
,
patch
,
NULL
);
if
(
new_note
)
{
r
=
_note_create
(
msg_mailbox
(
msg
),
new_note
,
&
updated_note
);
json_decref
(
new_note
);
}
else
{
r
=
IMAP_INTERNAL
;
syslog
(
LOG_ERR
,
"jmap: Notes/update(%s) patch: %s"
,
id
,
error_message
(
r
));
}
}
json_decref
(
note
);
if
(
!
r
)
{
const
struct
index_record
*
record
=
msg_record
(
msg
);
struct
mailbox
*
mailbox
=
msg_mailbox
(
msg
);
struct
index_record
newrecord
;
int
userflag
;
memcpy
(
&
newrecord
,
record
,
sizeof
(
struct
index_record
));
r
=
mailbox_user_flag
(
mailbox
,
DFLAG_UNBIND
,
&
userflag
,
1
);
if
(
!
r
)
{
newrecord
.
user_flags
[
userflag
/
32
]
|=
1
<<
(
userflag
&
31
);
newrecord
.
internal_flags
|=
FLAG_INTERNAL_EXPUNGED
;
r
=
mailbox_rewrite_index_record
(
mailbox
,
&
newrecord
);
}
}
if
(
r
)
{
err
=
json_pack
(
"{s:s, s:s}"
,
"type"
,
"serverFail"
,
"description"
,
error_message
(
r
));
}
}
if
(
err
)
{
json_object_set_new
(
set
->
not_updated
,
id
,
err
);
}
else
{
json_object_set_new
(
set
->
updated
,
id
,
updated_note
);
}
}
static
void
_notes_destroy_cb
(
const
char
*
id
,
message_t
*
msg
,
void
*
data
__attribute__
((
unused
)),
void
*
rock
)
{
struct
set_rock
*
srock
=
(
struct
set_rock
*
)
rock
;
struct
jmap_set
*
set
=
srock
->
set
;
json_t
*
err
=
NULL
;
if
((
srock
->
rights
&
JACL_REMOVEITEMS
)
!=
JACL_REMOVEITEMS
)
{
int
read_only
=
!
(
srock
->
rights
&
JACL_READITEMS
);
err
=
json_pack
(
"{s:s}"
,
"type"
,
read_only
?
"notFound"
:
"forbidden"
);
}
else
{
const
struct
index_record
*
record
=
msg_record
(
msg
);
struct
index_record
newrecord
;
memcpy
(
&
newrecord
,
record
,
sizeof
(
struct
index_record
));
newrecord
.
internal_flags
|=
FLAG_INTERNAL_EXPUNGED
;
int
r
=
mailbox_rewrite_index_record
(
msg_mailbox
(
msg
),
&
newrecord
);
if
(
r
)
{
err
=
json_pack
(
"{s:s, s:s}"
,
"type"
,
"serverFail"
,
"description"
,
error_message
(
r
));
}
}
if
(
err
)
{
json_object_set_new
(
set
->
not_destroyed
,
id
,
err
);
}
else
{
json_array_append_new
(
set
->
destroyed
,
json_string
(
id
));
}
}
static
int
jmap_notes_set
(
jmap_req_t
*
req
)
{
struct
jmap_parser
parser
=
JMAP_PARSER_INITIALIZER
;
struct
jmap_set
set
;
struct
mailbox
*
mbox
=
NULL
;
mbentry_t
*
mbentry
=
NULL
;
struct
buf
buf
=
BUF_INITIALIZER
;
json_t
*
err
=
NULL
;
int
rights
,
r
;
/* Parse request */
jmap_set_parse
(
req
,
&
parser
,
notes_props
,
NULL
,
NULL
,
&
set
,
&
err
);
if
(
err
)
{
jmap_error
(
req
,
err
);
goto
done
;
}
if
(
json_array_size
(
parser
.
invalid
))
{
err
=
json_pack
(
"{s:s}"
,
"type"
,
"invalidProperties"
);
json_object_set
(
err
,
"properties"
,
parser
.
invalid
);
jmap_error
(
req
,
err
);
goto
done
;
}
/* Process request */
r
=
ensure_notes_collection
(
req
->
accountid
,
&
mbentry
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"jmap_notes_set: ensure_notes_collection(%s): %s"
,
req
->
accountid
,
error_message
(
r
));
goto
done
;
}
rights
=
jmap_myrights_mbentry
(
req
,
mbentry
);
r
=
jmap_openmbox
(
req
,
mbentry
->
name
,
&
mbox
,
1
);
assert
(
mbox
);
mboxlist_entry_free
(
&
mbentry
);
if
(
r
)
goto
done
;
buf_printf
(
&
buf
,
MODSEQ_FMT
,
mbox
->
i
.
highestmodseq
);
set
.
old_state
=
buf_release
(
&
buf
);
if
(
set
.
if_in_state
&&
strcmp
(
set
.
if_in_state
,
set
.
old_state
))
{
jmap_error
(
req
,
json_pack
(
"{s:s}"
,
"type"
,
"stateMismatch"
));
goto
done
;
}
/* create */
const
char
*
creation_id
,
*
id
;
json_t
*
val
;
json_object_foreach
(
set
.
create
,
creation_id
,
val
)
{
json_t
*
new_note
=
NULL
;
if
((
rights
&
JACL_ADDITEMS
)
!=
JACL_ADDITEMS
)
{
err
=
json_pack
(
"{s:s}"
,
"type"
,
"forbidden"
);
}
else
if
(
!
_notes_setargs_check
(
NULL
/*id*/
,
val
,
&
err
))
{
r
=
_note_create
(
mbox
,
val
,
&
new_note
);
if
(
r
)
err
=
jmap_server_error
(
r
);
}
if
(
err
)
{
json_object_set_new
(
set
.
not_created
,
creation_id
,
err
);
}
else
{
/* Report note as created */
json_object_set_new
(
set
.
created
,
creation_id
,
new_note
);
/* Register creation id */
id
=
json_string_value
(
json_object_get
(
new_note
,
"id"
));
jmap_add_id
(
req
,
creation_id
,
id
);
}
}
/* update */
hash_table
ids
=
HASH_TABLE_INITIALIZER
;
struct
set_rock
srock
=
{
&
set
,
&
buf
,
rights
};
construct_hash_table
(
&
ids
,
32
,
0
);
/* Build hash of ids */
json_object_foreach
(
set
.
update
,
id
,
val
)
{
hash_insert
(
id
,
val
,
&
ids
);
}
/* Iterate through each message and update matching ids */
foreach_note
(
mbox
,
&
ids
,
&
_notes_update_cb
,
&
srock
);
/* Any remaining ids are not updated */
hash_enumerate
(
&
ids
,
&
not_found_cb
,
set
.
not_updated
);
free_hash_table
(
&
ids
,
NULL
);
/* destroy */
construct_hash_table
(
&
ids
,
32
,
0
);
/* Build hash of ids */
size_t
i
;
json_array_foreach
(
set
.
destroy
,
i
,
val
)
{
hash_insert
(
json_string_value
(
val
),
(
void
*
)
1
,
&
ids
);
}
/* Iterate through each message and destroy matching ids */
foreach_note
(
mbox
,
&
ids
,
&
_notes_destroy_cb
,
&
srock
);
/* Any remaining ids are not destroyed */
hash_enumerate
(
&
ids
,
&
not_found_cb
,
set
.
not_destroyed
);
free_hash_table
(
&
ids
,
NULL
);
/* force modseq to stable */
mailbox_unlock_index
(
mbox
,
NULL
);
/* Build response */
buf_reset
(
&
buf
);
buf_printf
(
&
buf
,
MODSEQ_FMT
,
mbox
->
i
.
highestmodseq
);
set
.
new_state
=
buf_release
(
&
buf
);
jmap_ok
(
req
,
jmap_set_reply
(
&
set
));
done
:
jmap_closembox
(
req
,
&
mbox
);
jmap_parser_fini
(
&
parser
);
jmap_set_fini
(
&
set
);
return
0
;
}
struct
change
{
modseq_t
modseq
;
json_t
*
id
;
json_t
*
list
;
};
static
int
change_cmp
(
const
void
**
a
,
const
void
**
b
)
{
const
struct
change
*
chg_a
=
(
const
struct
change
*
)
*
a
;
const
struct
change
*
chg_b
=
(
const
struct
change
*
)
*
b
;
return
(
chg_a
-
chg_b
);
}
static
int
jmap_notes_changes
(
jmap_req_t
*
req
)
{
struct
jmap_parser
parser
=
JMAP_PARSER_INITIALIZER
;
struct
jmap_changes
changes
;
struct
mailbox
*
mbox
=
NULL
;
mbentry_t
*
mbentry
=
NULL
;
int
userflag
;
json_t
*
err
=
NULL
;
jmap_changes_parse
(
req
,
&
parser
,
req
->
counters
.
notesdeletedmodseq
,
NULL
,
NULL
,
&
changes
,
&
err
);
if
(
err
)
{
jmap_error
(
req
,
err
);
goto
done
;
}
int
r
=
ensure_notes_collection
(
req
->
accountid
,
&
mbentry
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"jmap_notes_changes: ensure_submission_collection(%s): %s"
,
req
->
accountid
,
error_message
(
r
));
goto
done
;
}
r
=
jmap_openmbox
(
req
,
mbentry
->
name
,
&
mbox
,
0
);
mboxlist_entry_free
(
&
mbentry
);
r
=
mailbox_user_flag
(
mbox
,
DFLAG_UNBIND
,
&
userflag
,
0
);
if
(
r
)
userflag
=
-1
;
if
(
r
)
goto
done
;
struct
mailbox_iter
*
iter
=
mailbox_iter_init
(
mbox
,
changes
.
since_modseq
,
0
);
modseq_t
highest_modseq
=
mbox
->
i
.
highestmodseq
;
ptrarray_t
changed_msgs
=
PTRARRAY_INITIALIZER
;
struct
buf
buf
=
BUF_INITIALIZER
;
const
message_t
*
msg
;
while
((
msg
=
mailbox_iter_step
(
iter
)))
{
const
struct
index_record
*
record
=
msg_record
(
msg
);
json_t
*
change_list
;
const
char
*
id
;
/* Determine the type of change */
if
(
record
->
internal_flags
&
FLAG_INTERNAL_EXPUNGED
)
{
/* Skip any notes that have been replaced by an update */
if
(
userflag
>=
0
&&
record
->
user_flags
[
userflag
/
32
]
&
(
1
<<
(
userflag
&
31
)))
continue
;
/* Skip any notes created AND deleted since modseq */
if
(
record
->
createdmodseq
>
changes
.
since_modseq
)
continue
;
change_list
=
changes
.
destroyed
;
}
else
if
(
record
->
createdmodseq
>
changes
.
since_modseq
)
{
change_list
=
changes
.
created
;
}
else
{
change_list
=
changes
.
updated
;
}
/* Fetch note uid */
if
(
message_get_field
((
message_t
*
)
msg
,
"X-Universally-Unique-Identifier"
,
MESSAGE_DECODED
|
MESSAGE_TRIM
,
&
buf
))
continue
;
id
=
buf_cstring
(
&
buf
);
if
(
changes
.
max_changes
)
{
/* Add change to a list to be sorted nby modseq later */
struct
change
*
change
=
xmalloc
(
sizeof
(
struct
change
));
ptrarray_append
(
&
changed_msgs
,
change
);
change
->
modseq
=
record
->
modseq
;
change
->
id
=
json_string
(
id
);
change
->
list
=
change_list
;
}
else
{
/* Add change to the proper array in the response */
json_array_append_new
(
change_list
,
json_string
(
id
));
}
}
mailbox_iter_done
(
&
iter
);
jmap_closembox
(
req
,
&
mbox
);
buf_free
(
&
buf
);
if
(
changes
.
max_changes
)
{
int
i
;
if
((
size_t
)
ptrarray_size
(
&
changed_msgs
)
>
changes
.
max_changes
)
{
/* Sort changes by modseq */
changes
.
has_more_changes
=
1
;
ptrarray_sort
(
&
changed_msgs
,
&
change_cmp
);
/* Determine where to cutoff our list of changes
(MUST NOT split changes having the same modseq) */
struct
change
*
change
=
ptrarray_nth
(
&
changed_msgs
,
changes
.
max_changes
);
highest_modseq
=
change
->
modseq
;
for
(
i
=
changes
.
max_changes
-
1
;
i
>=
0
;
i
--
)
{
change
=
ptrarray_nth
(
&
changed_msgs
,
i
);
if
(
change
->
modseq
<
highest_modseq
)
{
highest_modseq
=
change
->
modseq
;
changes
.
max_changes
=
i
+
1
;
break
;
}
}
if
(
i
<
0
)
{
/* too many changes */
}
}
/* Output and/or free the changes */
for
(
i
=
0
;
i
<
ptrarray_size
(
&
changed_msgs
);
i
++
)
{
struct
change
*
change
=
ptrarray_nth
(
&
changed_msgs
,
i
);
if
((
size_t
)
i
<
changes
.
max_changes
)
{
/* Add change to the proper array in the response */
json_array_append_new
(
change
->
list
,
change
->
id
);
}
else
{
/* Throw this change away */
json_decref
(
change
->
id
);
}
free
(
change
);
}
}
ptrarray_fini
(
&
changed_msgs
);
/* Set new state */
changes
.
new_modseq
=
highest_modseq
;
jmap_ok
(
req
,
jmap_changes_reply
(
&
changes
));
done
:
jmap_changes_fini
(
&
changes
);
jmap_parser_fini
(
&
parser
);
return
0
;
}
File Metadata
Details
Attached
Mime Type
text/x-c
Expires
Fri, Apr 24, 1:14 PM (3 d, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18883472
Default Alt Text
jmap_notes.c (32 KB)
Attached To
Mode
R111 cyrus-imapd
Attached
Detach File
Event Timeline