Release 1.25.0 -- Buddy Idle Time, RTF
[siplcs.git] / src / core / sipe-ocs2005.c
blob9dfab368b24b0834633343ef1fff0e51c825850d
1 /**
2 * @file sipe-ocs2005.c
4 * pidgin-sipe
6 * Copyright (C) 2011-2019 SIPE Project <http://sipe.sourceforge.net/>
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 * OCS2005 specific code
28 #include <time.h>
30 #include <glib.h>
32 #include "sipe-common.h"
33 #include "sip-soap.h"
34 #include "sip-transport.h"
35 #include "sipe-backend.h"
36 #include "sipe-buddy.h"
37 #include "sipe-cal.h"
38 #include "sipe-core.h"
39 #include "sipe-core-private.h"
40 #include "sipe-ews.h"
41 #include "sipe-ocs2005.h"
42 #include "sipe-ocs2007.h"
43 #include "sipe-schedule.h"
44 #include "sipe-status.h"
45 #include "sipe-utils.h"
46 #include "sipe-xml.h"
48 /**
49 * 2005-style Activity and Availability.
51 * [MS-SIP] 2.2.1
53 * @param activity 2005 aggregated activity. Ex.: 600
54 * @param availablity 2005 aggregated availablity. Ex.: 300
56 * The values define the starting point of a range
58 #define SIPE_OCS2005_ACTIVITY_UNKNOWN 0
59 #define SIPE_OCS2005_ACTIVITY_AWAY 100
60 #define SIPE_OCS2005_ACTIVITY_LUNCH 150
61 #define SIPE_OCS2005_ACTIVITY_IDLE 200
62 #define SIPE_OCS2005_ACTIVITY_BRB 300
63 #define SIPE_OCS2005_ACTIVITY_AVAILABLE 400 /* user is active */
64 #define SIPE_OCS2005_ACTIVITY_ON_PHONE 500 /* user is participating in a communcation session */
65 #define SIPE_OCS2005_ACTIVITY_BUSY 600
66 #define SIPE_OCS2005_ACTIVITY_AWAY2 700
67 #define SIPE_OCS2005_ACTIVITY_AVAILABLE2 800
69 #define SIPE_OCS2005_AVAILABILITY_OFFLINE 0
70 #define SIPE_OCS2005_AVAILABILITY_MAYBE 100
71 #define SIPE_OCS2005_AVAILABILITY_ONLINE 300
72 static guint sipe_ocs2005_activity_from_status(struct sipe_core_private *sipe_private)
74 const gchar *status = sipe_private->status;
76 if (sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_AWAY))) {
77 return(SIPE_OCS2005_ACTIVITY_AWAY);
78 /*} else if (sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_LUNCH))) {
79 return(SIPE_OCS2005_ACTIVITY_LUNCH); */
80 } else if (sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_BRB))) {
81 return(SIPE_OCS2005_ACTIVITY_BRB);
82 } else if (sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_AVAILABLE))) {
83 return(SIPE_OCS2005_ACTIVITY_AVAILABLE);
84 /*} else if (sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_ON_PHONE))) {
85 return(SIPE_OCS2005_ACTIVITY_ON_PHONE); */
86 } else if (sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_BUSY)) ||
87 sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_DND))) {
88 return(SIPE_OCS2005_ACTIVITY_BUSY);
89 } else if (sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_INVISIBLE)) ||
90 sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_OFFLINE))) {
91 return(SIPE_OCS2005_ACTIVITY_AWAY);
92 } else {
93 return(SIPE_OCS2005_ACTIVITY_AVAILABLE);
97 static guint sipe_ocs2005_availability_from_status(struct sipe_core_private *sipe_private)
99 const gchar *status = sipe_private->status;
101 if (sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_INVISIBLE)) ||
102 sipe_strequal(status, sipe_status_activity_to_token(SIPE_ACTIVITY_OFFLINE)))
103 return(SIPE_OCS2005_AVAILABILITY_OFFLINE);
104 else
105 return(SIPE_OCS2005_AVAILABILITY_ONLINE);
108 const gchar *sipe_ocs2005_status_from_activity_availability(guint activity,
109 guint availability)
111 guint type;
113 if (availability < SIPE_OCS2005_AVAILABILITY_MAYBE) {
114 type = SIPE_ACTIVITY_OFFLINE;
115 } else if (activity < SIPE_OCS2005_ACTIVITY_LUNCH) {
116 type = SIPE_ACTIVITY_AWAY;
117 } else if (activity < SIPE_OCS2005_ACTIVITY_IDLE) {
118 //type = SIPE_ACTIVITY_LUNCH;
119 type = SIPE_ACTIVITY_AWAY;
120 } else if (activity < SIPE_OCS2005_ACTIVITY_BRB) {
121 //type = SIPE_ACTIVITY_IDLE;
122 type = SIPE_ACTIVITY_AWAY;
123 } else if (activity < SIPE_OCS2005_ACTIVITY_AVAILABLE) {
124 type = SIPE_ACTIVITY_BRB;
125 } else if (activity < SIPE_OCS2005_ACTIVITY_ON_PHONE) {
126 type = SIPE_ACTIVITY_AVAILABLE;
127 } else if (activity < SIPE_OCS2005_ACTIVITY_BUSY) {
128 //type = SIPE_ACTIVITY_ON_PHONE;
129 type = SIPE_ACTIVITY_BUSY;
130 } else if (activity < SIPE_OCS2005_ACTIVITY_AWAY2) {
131 type = SIPE_ACTIVITY_BUSY;
132 } else if (activity < SIPE_OCS2005_ACTIVITY_AVAILABLE2) {
133 type = SIPE_ACTIVITY_AWAY;
134 } else {
135 type = SIPE_ACTIVITY_AVAILABLE;
138 return(sipe_status_activity_to_token(type));
141 const gchar *sipe_ocs2005_activity_description(guint activity)
143 if ((activity >= SIPE_OCS2005_ACTIVITY_LUNCH) &&
144 (activity < SIPE_OCS2005_ACTIVITY_IDLE)) {
145 return(sipe_core_activity_description(SIPE_ACTIVITY_LUNCH));
146 } else if ((activity >= SIPE_OCS2005_ACTIVITY_IDLE) &&
147 (activity < SIPE_OCS2005_ACTIVITY_BRB)) {
148 return(sipe_core_activity_description(SIPE_ACTIVITY_INACTIVE));
149 } else if ((activity >= SIPE_OCS2005_ACTIVITY_ON_PHONE) &&
150 (activity < SIPE_OCS2005_ACTIVITY_BUSY)) {
151 return(sipe_core_activity_description(SIPE_ACTIVITY_ON_PHONE));
152 } else {
153 return(NULL);
157 void sipe_ocs2005_user_info_has_updated(struct sipe_core_private *sipe_private,
158 const sipe_xml *xn_userinfo)
160 const sipe_xml *xn_states;
162 g_free(sipe_private->ocs2005_user_states);
163 sipe_private->ocs2005_user_states = NULL;
164 if ((xn_states = sipe_xml_child(xn_userinfo, "states")) != NULL) {
165 gchar *orig = sipe_private->ocs2005_user_states = sipe_xml_stringify(xn_states);
167 /* this is a hack-around to remove added newline after inner element,
168 * state in this case, where it shouldn't be.
169 * After several use of sipe_xml_stringify, amount of added newlines
170 * grows significantly.
172 if (orig) {
173 gchar c, *stripped = orig;
174 while ((c = *orig++)) {
175 if ((c != '\n') /* && (c != '\r') */) {
176 *stripped++ = c;
179 *stripped = '\0';
183 /* Publish initial state if not yet.
184 * Assuming this happens on initial responce to self subscription
185 * so we've already updated our UserInfo.
187 if (!SIPE_CORE_PRIVATE_FLAG_IS(INITIAL_PUBLISH)) {
188 sipe_ocs2005_presence_publish(sipe_private, FALSE);
189 /* dalayed run */
190 sipe_cal_delayed_calendar_update(sipe_private);
194 static gboolean sipe_is_user_available(struct sipe_core_private *sipe_private)
196 return(sipe_strequal(sipe_private->status,
197 sipe_status_activity_to_token(SIPE_ACTIVITY_AVAILABLE)));
202 * OCS2005 presence XML messages
204 * Calendar publication entry
206 * @param legacy_dn (%s) Ex.: /o=EXCHANGE/ou=BTUK02/cn=Recipients/cn=AHHBTT
207 * @param fb_start_time_str (%s) Ex.: 2009-12-06T17:15:00Z
208 * @param free_busy_base64 (%s) Ex.: AAAAAAAAAAAAAAAAA......
210 #define SIPE_SOAP_SET_PRESENCE_CALENDAR \
211 "<calendarInfo xmlns=\"http://schemas.microsoft.com/2002/09/sip/presence\" mailboxId=\"%s\" startTime=\"%s\" granularity=\"PT15M\">%s</calendarInfo>"
214 * Note publication entry
216 * @param note (%s) Ex.: Working from home
218 #define SIPE_SOAP_SET_PRESENCE_NOTE_XML "<note>%s</note>"
221 * Note's OOF publication entry
223 #define SIPE_SOAP_SET_PRESENCE_OOF_XML "<oof></oof>"
226 * States publication entry for User State
228 * @param avail (%d) Availability 2007-style. Ex.: 9500
229 * @param since_time_str (%s) Ex.: 2010-01-13T10:30:05Z
230 * @param device_id (%s) epid. Ex.: 4c77e6ec72
231 * @param activity_token (%s) Ex.: do-not-disturb
233 #define SIPE_SOAP_SET_PRESENCE_STATES \
234 "<states>"\
235 "<state avail=\"%d\" since=\"%s\" validWith=\"any-device\" deviceId=\"%s\" set=\"manual\" xsi:type=\"userState\">%s</state>"\
236 "</states>"
239 * Presentity publication entry.
241 * @param uri (%s) SIP URI without 'sip:' prefix. Ex.: fox@atlanta.local
242 * @param aggr_availability (%d) Ex.: 300
243 * @param aggr_activity (%d) Ex.: 600
244 * @param host_name (%s) Uppercased. Ex.: ATLANTA
245 * @param note_xml_str (%s) XML string as SIPE_SOAP_SET_PRESENCE_NOTE_XML
246 * @param oof_xml_str (%s) XML string as SIPE_SOAP_SET_PRESENCE_OOF_XML
247 * @param states_xml_str (%s) XML string as SIPE_SOAP_SET_PRESENCE_STATES
248 * @param calendar_info_xml_str (%s) XML string as SIPE_SOAP_SET_PRESENCE_CALENDAR
249 * @param device_id (%s) epid. Ex.: 4c77e6ec72
250 * @param since_time_str (%s) Ex.: 2010-01-13T10:30:05Z
251 * @param since_time_str (%s) Ex.: 2010-01-13T10:30:05Z
252 * @param user_input (%s) active, idle
254 #define SIPE_SOAP_SET_PRESENCE \
255 "<s:Envelope" \
256 " xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"" \
257 " xmlns:m=\"http://schemas.microsoft.com/winrtc/2002/11/sip\"" \
258 ">" \
259 "<s:Body>" \
260 "<m:setPresence>" \
261 "<m:presentity xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" m:uri=\"sip:%s\">"\
262 "<m:availability m:aggregate=\"%d\"/>"\
263 "<m:activity m:aggregate=\"%d\"/>"\
264 "<deviceName xmlns=\"http://schemas.microsoft.com/2002/09/sip/presence\" name=\"%s\"/>"\
265 "<rtc:devicedata xmlns:rtc=\"http://schemas.microsoft.com/winrtc/2002/11/sip\" namespace=\"rtcService\">"\
266 "<![CDATA[<caps><renders_gif/><renders_isf/></caps>]]></rtc:devicedata>"\
267 "<userInfo xmlns=\"http://schemas.microsoft.com/2002/09/sip/presence\">"\
268 "%s%s" \
269 "%s" \
270 "</userInfo>"\
271 "%s" \
272 "<device xmlns=\"http://schemas.microsoft.com/2002/09/sip/presence\" deviceId=\"%s\" since=\"%s\" >"\
273 "<userInput since=\"%s\" >%s</userInput>"\
274 "</device>"\
275 "</m:presentity>" \
276 "</m:setPresence>"\
277 "</s:Body>" \
278 "</s:Envelope>"
280 static void send_presence_soap(struct sipe_core_private *sipe_private,
281 gboolean do_publish_calendar,
282 gboolean do_reset_status)
284 struct sipe_calendar* cal = sipe_private->calendar;
285 gchar *body;
286 gchar *tmp;
287 gchar *tmp2 = NULL;
288 gchar *res_note = NULL;
289 const gchar *res_oof = NULL;
290 const gchar *note_pub = NULL;
291 gchar *states = NULL;
292 gchar *calendar_data = NULL;
293 const gchar *epid = sip_transport_epid(sipe_private);
294 gchar *from = sip_uri_self(sipe_private);
295 time_t now = time(NULL);
296 gchar *since_time_str = sipe_utils_time_to_str(now);
297 const gchar *oof_note = cal ? sipe_ews_get_oof_note(cal) : NULL;
298 const char *user_input;
299 gboolean pub_oof = cal && oof_note && (!sipe_private->note || cal->updated > sipe_private->note_since);
301 if (oof_note && sipe_private->note) {
302 SIPE_DEBUG_INFO("cal->oof_start : %s", sipe_utils_time_to_debug_str(localtime(&(cal->oof_start))));
303 SIPE_DEBUG_INFO("sipe_private->note_since : %s", sipe_utils_time_to_debug_str(localtime(&(sipe_private->note_since))));
306 SIPE_DEBUG_INFO("sipe_private->note : %s", sipe_private->note ? sipe_private->note : "");
308 if (!SIPE_CORE_PRIVATE_FLAG_IS(INITIAL_PUBLISH) ||
309 do_reset_status)
310 sipe_status_set_activity(sipe_private, SIPE_ACTIVITY_AVAILABLE);
312 /* Note */
313 if (pub_oof) {
314 note_pub = oof_note;
315 res_oof = SIPE_SOAP_SET_PRESENCE_OOF_XML;
316 cal->published = TRUE;
317 } else if (sipe_private->note) {
318 if (SIPE_CORE_PRIVATE_FLAG_IS(OOF_NOTE) &&
319 !oof_note) { /* stale OOF note, as it's not present in cal already */
320 g_free(sipe_private->note);
321 sipe_private->note = NULL;
322 SIPE_CORE_PRIVATE_FLAG_UNSET(OOF_NOTE);
323 sipe_private->note_since = 0;
324 } else {
325 note_pub = sipe_private->note;
326 res_oof = SIPE_CORE_PRIVATE_FLAG_IS(OOF_NOTE) ? SIPE_SOAP_SET_PRESENCE_OOF_XML : "";
330 if (note_pub)
332 /* to protocol internal plain text format */
333 tmp = sipe_backend_markup_strip_html(note_pub);
334 res_note = g_markup_printf_escaped(SIPE_SOAP_SET_PRESENCE_NOTE_XML, tmp);
335 g_free(tmp);
338 /* User State */
339 if (!do_reset_status) {
340 if (sipe_private->status_set_by_user &&
341 !do_publish_calendar &&
342 SIPE_CORE_PRIVATE_FLAG_IS(INITIAL_PUBLISH)) {
343 const gchar *activity_token;
344 int avail_2007 = sipe_ocs2007_availability_from_status(sipe_private->status,
345 &activity_token);
347 states = g_strdup_printf(SIPE_SOAP_SET_PRESENCE_STATES,
348 avail_2007,
349 since_time_str,
350 epid,
351 activity_token);
353 else /* preserve existing publication */
355 if (sipe_private->ocs2005_user_states) {
356 states = g_strdup(sipe_private->ocs2005_user_states);
359 } else {
360 /* do nothing - then User state will be erased */
362 SIPE_CORE_PRIVATE_FLAG_SET(INITIAL_PUBLISH);
364 /* CalendarInfo */
365 if (cal && (!is_empty(cal->legacy_dn) || !is_empty(cal->email)) && cal->fb_start && !is_empty(cal->free_busy))
367 char *fb_start_str = sipe_utils_time_to_str(cal->fb_start);
368 char *free_busy_base64 = sipe_cal_get_freebusy_base64(cal->free_busy);
369 calendar_data = g_strdup_printf(SIPE_SOAP_SET_PRESENCE_CALENDAR,
370 !is_empty(cal->legacy_dn) ? cal->legacy_dn : cal->email,
371 fb_start_str,
372 free_busy_base64);
373 g_free(fb_start_str);
374 g_free(free_busy_base64);
377 user_input = (sipe_private->status_set_by_user ||
378 sipe_is_user_available(sipe_private)) ?
379 "active" : "idle";
381 /* generate XML */
382 body = g_strdup_printf(SIPE_SOAP_SET_PRESENCE,
383 sipe_private->username,
384 sipe_ocs2005_availability_from_status(sipe_private),
385 sipe_ocs2005_activity_from_status(sipe_private),
386 (tmp = g_ascii_strup(g_get_host_name(), -1)),
387 res_note ? res_note : "",
388 res_oof ? res_oof : "",
389 states ? states : "",
390 calendar_data ? calendar_data : "",
391 epid,
392 since_time_str,
393 since_time_str,
394 user_input);
395 g_free(tmp);
396 g_free(tmp2);
397 g_free(res_note);
398 g_free(states);
399 g_free(calendar_data);
400 g_free(since_time_str);
402 sip_soap_raw_request_cb(sipe_private, from, body, NULL, NULL);
404 g_free(body);
407 void sipe_ocs2005_presence_publish(struct sipe_core_private *sipe_private,
408 gboolean do_publish_calendar)
410 send_presence_soap(sipe_private, do_publish_calendar, FALSE);
413 void sipe_ocs2005_reset_status(struct sipe_core_private *sipe_private)
415 send_presence_soap(sipe_private, FALSE, TRUE);
418 void sipe_ocs2005_apply_calendar_status(struct sipe_core_private *sipe_private,
419 struct sipe_buddy *sbuddy,
420 const char *status_id)
422 time_t cal_avail_since;
423 int cal_status = sipe_cal_get_status(sbuddy, time(NULL), &cal_avail_since);
424 int avail;
425 gchar *self_uri;
427 if (!sbuddy) return;
429 if (cal_status < SIPE_CAL_NO_DATA) {
430 SIPE_DEBUG_INFO("sipe_apply_calendar_status: cal_status : %d for %s", cal_status, sbuddy->name);
431 SIPE_DEBUG_INFO("sipe_apply_calendar_status: cal_avail_since : %s", sipe_utils_time_to_debug_str(localtime(&cal_avail_since)));
434 /* scheduled Cal update call */
435 if (!status_id) {
436 status_id = sbuddy->last_non_cal_status_id;
437 g_free(sbuddy->activity);
438 sbuddy->activity = g_strdup(sbuddy->last_non_cal_activity);
441 if (!status_id) {
442 SIPE_DEBUG_INFO("sipe_apply_calendar_status: status_id is NULL for %s, exiting.",
443 sbuddy->name ? sbuddy->name : "" );
444 return;
447 /* adjust to calendar status */
448 if (cal_status != SIPE_CAL_NO_DATA) {
449 SIPE_DEBUG_INFO("sipe_apply_calendar_status: user_avail_since: %s", sipe_utils_time_to_debug_str(localtime(&sbuddy->user_avail_since)));
451 if ((cal_status == SIPE_CAL_BUSY) &&
452 (cal_avail_since > sbuddy->user_avail_since) &&
453 sipe_ocs2007_status_is_busy(status_id)) {
454 status_id = sipe_status_activity_to_token(SIPE_ACTIVITY_BUSY);
455 g_free(sbuddy->activity);
456 sbuddy->activity = g_strdup(sipe_core_activity_description(SIPE_ACTIVITY_IN_MEETING));
458 avail = sipe_ocs2007_availability_from_status(status_id, NULL);
460 SIPE_DEBUG_INFO("sipe_apply_calendar_status: activity_since : %s", sipe_utils_time_to_debug_str(localtime(&sbuddy->activity_since)));
461 if (cal_avail_since > sbuddy->activity_since) {
462 if ((cal_status == SIPE_CAL_OOF) &&
463 sipe_ocs2007_availability_is_away(avail)) {
464 g_free(sbuddy->activity);
465 sbuddy->activity = g_strdup(sipe_core_activity_description(SIPE_ACTIVITY_OOF));
470 /* then set status_id actually */
471 SIPE_DEBUG_INFO("sipe_apply_calendar_status: to %s for %s", status_id, sbuddy->name ? sbuddy->name : "" );
472 sipe_backend_buddy_set_status(SIPE_CORE_PUBLIC, sbuddy->name,
473 sipe_status_token_to_activity(status_id),
476 /* set our account state to the one in roaming (including calendar info) */
477 self_uri = sip_uri_self(sipe_private);
478 if (SIPE_CORE_PRIVATE_FLAG_IS(INITIAL_PUBLISH) &&
479 sipe_strcase_equal(sbuddy->name, self_uri)) {
480 if (sipe_strequal(status_id, sipe_status_activity_to_token(SIPE_ACTIVITY_OFFLINE))) {
481 /* do not let offline status switch us off */
482 status_id = sipe_status_activity_to_token(SIPE_ACTIVITY_INVISIBLE);
485 sipe_status_and_note(sipe_private, status_id);
487 g_free(self_uri);
490 static void update_calendar_status_cb(SIPE_UNUSED_PARAMETER char *name,
491 struct sipe_buddy *sbuddy,
492 struct sipe_core_private *sipe_private)
494 sipe_ocs2005_apply_calendar_status(sipe_private, sbuddy, NULL);
498 * Updates contact's status
499 * based on their calendar information.
501 static void update_calendar_status(struct sipe_core_private *sipe_private,
502 SIPE_UNUSED_PARAMETER void *unused)
504 SIPE_DEBUG_INFO_NOFORMAT("update_calendar_status() started.");
505 sipe_buddy_foreach(sipe_private,
506 (GHFunc) update_calendar_status_cb,
507 sipe_private);
509 /* repeat scheduling */
510 sipe_ocs2005_schedule_status_update(sipe_private,
511 time(NULL) + 3 * 60 /* 3 min */);
515 * Schedules process of contacts' status update
516 * based on their calendar information.
517 * Should be scheduled to the beginning of every
518 * 15 min interval, like:
519 * 13:00, 13:15, 13:30, 13:45, etc.
521 void sipe_ocs2005_schedule_status_update(struct sipe_core_private *sipe_private,
522 time_t calculate_from)
524 #define SCHEDULE_INTERVAL 15 * 60 /* 15 min */
526 /* start of the beginning of closest 15 min interval. */
527 time_t next_start = (calculate_from / SCHEDULE_INTERVAL + 1) * SCHEDULE_INTERVAL;
529 SIPE_DEBUG_INFO("sipe_ocs2005_schedule_status_update: calculate_from time: %s",
530 sipe_utils_time_to_debug_str(localtime(&calculate_from)));
531 SIPE_DEBUG_INFO("sipe_ocs2005_schedule_status_update: next start time : %s",
532 sipe_utils_time_to_debug_str(localtime(&next_start)));
534 sipe_schedule_seconds(sipe_private,
535 "<+2005-cal-status>",
536 NULL,
537 next_start - time(NULL),
538 update_calendar_status,
539 NULL);
543 Local Variables:
544 mode: c
545 c-file-style: "bsd"
546 indent-tabs-mode: t
547 tab-width: 8
548 End: