1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
pub use mailpot::PATH_SEGMENT;
21
use percent_encoding::utf8_percent_encode;
22

            
23
use super::*;
24

            
25
pub trait IntoCrumb: TypedPath {
26
53
    fn to_crumb(&self) -> Cow<'static, str> {
27
53
        Cow::from(self.to_uri().to_string())
28
53
    }
29
}
30

            
31
impl<TP: TypedPath> IntoCrumb for TP {}
32

            
33
5
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
34
#[serde(untagged)]
35
pub enum ListPathIdentifier {
36
5
    Pk(#[serde(deserialize_with = "parse_int")] i64),
37
    Id(String),
38
}
39

            
40
5
fn parse_int<'de, T, D>(de: D) -> Result<T, D::Error>
41
where
42
    D: serde::Deserializer<'de>,
43
    T: std::str::FromStr,
44
    <T as std::str::FromStr>::Err: std::fmt::Display,
45
{
46
    use serde::Deserialize;
47
5
    String::deserialize(de)?
48
        .parse()
49
        .map_err(serde::de::Error::custom)
50
5
}
51

            
52
impl From<i64> for ListPathIdentifier {
53
    fn from(val: i64) -> Self {
54
        Self::Pk(val)
55
    }
56
}
57

            
58
impl From<String> for ListPathIdentifier {
59
8
    fn from(val: String) -> Self {
60
8
        Self::Id(val)
61
8
    }
62
}
63

            
64
impl std::fmt::Display for ListPathIdentifier {
65
    #[allow(clippy::unnecessary_to_owned)]
66
25
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67
25
        let id: Cow<'_, str> = match self {
68
            Self::Pk(id) => id.to_string().into(),
69
25
            Self::Id(id) => id.into(),
70
        };
71
25
        write!(f, "{}", utf8_percent_encode(&id, PATH_SEGMENT,))
72
25
    }
73
}
74

            
75
5
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
76
8
#[typed_path("/list/:id/")]
77
pub struct ListPath(pub ListPathIdentifier);
78

            
79
impl From<&DbVal<mailpot::models::MailingList>> for ListPath {
80
2
    fn from(val: &DbVal<mailpot::models::MailingList>) -> Self {
81
2
        Self(ListPathIdentifier::Id(val.id.clone()))
82
2
    }
83
}
84

            
85
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
86
5
#[typed_path("/list/:id/posts/:msgid/")]
87
pub struct ListPostPath(pub ListPathIdentifier, pub String);
88

            
89
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
90
1
#[typed_path("/list/:id/posts/:msgid/raw/")]
91
pub struct ListPostRawPath(pub ListPathIdentifier, pub String);
92

            
93
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
94
1
#[typed_path("/list/:id/posts/:msgid/eml/")]
95
pub struct ListPostEmlPath(pub ListPathIdentifier, pub String);
96

            
97
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
98
7
#[typed_path("/list/:id/edit/")]
99
pub struct ListEditPath(pub ListPathIdentifier);
100

            
101
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
102
1
#[typed_path("/list/:id/edit/subscribers/")]
103
pub struct ListEditSubscribersPath(pub ListPathIdentifier);
104

            
105
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
106
1
#[typed_path("/list/:id/edit/candidates/")]
107
pub struct ListEditCandidatesPath(pub ListPathIdentifier);
108

            
109
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
110
1
#[typed_path("/settings/list/:id/")]
111
pub struct ListSettingsPath(pub ListPathIdentifier);
112

            
113
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
114
15
#[typed_path("/login/")]
115
pub struct LoginPath;
116

            
117
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
118
#[typed_path("/logout/")]
119
pub struct LogoutPath;
120

            
121
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
122
11
#[typed_path("/settings/")]
123
pub struct SettingsPath;
124

            
125
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
126
9
#[typed_path("/help/")]
127
pub struct HelpPath;
128

            
129
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
130
3
#[typed_path("/topics/")]
131
pub struct TopicsPath;
132

            
133
macro_rules! unit_impl {
134
    ($ident:ident, $ty:expr) => {
135
20
        pub fn $ident(state: &minijinja::State) -> std::result::Result<Value, Error> {
136
20
            urlize(state, Value::from($ty.to_crumb().to_string()))
137
20
        }
138
    };
139
}
140

            
141
unit_impl!(login_path, LoginPath);
142
unit_impl!(logout_path, LogoutPath);
143
unit_impl!(settings_path, SettingsPath);
144
unit_impl!(help_path, HelpPath);
145

            
146
macro_rules! list_id_impl {
147
    ($ident:ident, $ty:tt) => {
148
7
        pub fn $ident(state: &minijinja::State, id: Value) -> std::result::Result<Value, Error> {
149
7
            urlize(
150
                state,
151
7
                if let Some(id) = id.as_str() {
152
7
                    Value::from(
153
7
                        $ty(ListPathIdentifier::Id(id.to_string()))
154
7
                            .to_crumb()
155
7
                            .to_string(),
156
                    )
157
7
                } else {
158
                    let pk = id.try_into()?;
159
                    Value::from($ty(ListPathIdentifier::Pk(pk)).to_crumb().to_string())
160
                },
161
            )
162
7
        }
163
    };
164
}
165

            
166
list_id_impl!(list_path, ListPath);
167
list_id_impl!(list_settings_path, ListSettingsPath);
168
list_id_impl!(list_edit_path, ListEditPath);
169
list_id_impl!(list_subscribers_path, ListEditSubscribersPath);
170
list_id_impl!(list_candidates_path, ListEditCandidatesPath);
171

            
172
macro_rules! list_post_impl {
173
    ($ident:ident, $ty:tt) => {
174
5
        pub fn $ident(
175
            state: &minijinja::State,
176
            id: Value,
177
            msg_id: Value,
178
        ) -> std::result::Result<Value, Error> {
179
5
            urlize(state, {
180
10
                let Some(msg_id) = msg_id.as_str().map(|s| {
181
5
                    if s.starts_with('<') && s.ends_with('>') {
182
3
                        s.to_string()
183
                    } else {
184
2
                        format!("<{s}>")
185
                    }
186
5
                }) else {
187
                    return Err(Error::new(
188
                        minijinja::ErrorKind::UnknownMethod,
189
                        "Second argument of list_post_path must be a string.",
190
                    ));
191
                };
192

            
193
5
                if let Some(id) = id.as_str() {
194
5
                    Value::from(
195
5
                        $ty(ListPathIdentifier::Id(id.to_string()), msg_id)
196
5
                            .to_crumb()
197
5
                            .to_string(),
198
                    )
199
5
                } else {
200
                    let pk = id.try_into()?;
201
                    Value::from(
202
                        $ty(ListPathIdentifier::Pk(pk), msg_id)
203
                            .to_crumb()
204
                            .to_string(),
205
                    )
206
                }
207
            })
208
5
        }
209
    };
210
}
211

            
212
list_post_impl!(list_post_path, ListPostPath);
213
list_post_impl!(post_raw_path, ListPostRawPath);
214
list_post_impl!(post_eml_path, ListPostEmlPath);
215

            
216
pub mod tsr {
217
    use std::{borrow::Cow, convert::Infallible};
218

            
219
    use axum::{
220
        http::Request,
221
        response::{IntoResponse, Redirect, Response},
222
        routing::{any, MethodRouter},
223
        Router,
224
    };
225
    use axum_extra::routing::{RouterExt as ExtraRouterExt, SecondElementIs, TypedPath};
226
    use http::{uri::PathAndQuery, StatusCode, Uri};
227
    use tower_service::Service;
228

            
229
    /// Extension trait that adds additional methods to [`Router`].
230
    pub trait RouterExt<S, B>: ExtraRouterExt<S, B> {
231
        /// Add a typed `GET` route to the router.
232
        ///
233
        /// The path will be inferred from the first argument to the handler
234
        /// function which must implement [`TypedPath`].
235
        ///
236
        /// See [`TypedPath`] for more details and examples.
237
        fn typed_get<H, T, P>(self, handler: H) -> Self
238
        where
239
            H: axum::handler::Handler<T, S, B>,
240
            T: SecondElementIs<P> + 'static,
241
            P: TypedPath;
242

            
243
        /// Add a typed `DELETE` route to the router.
244
        ///
245
        /// The path will be inferred from the first argument to the handler
246
        /// function which must implement [`TypedPath`].
247
        ///
248
        /// See [`TypedPath`] for more details and examples.
249
        fn typed_delete<H, T, P>(self, handler: H) -> Self
250
        where
251
            H: axum::handler::Handler<T, S, B>,
252
            T: SecondElementIs<P> + 'static,
253
            P: TypedPath;
254

            
255
        /// Add a typed `HEAD` route to the router.
256
        ///
257
        /// The path will be inferred from the first argument to the handler
258
        /// function which must implement [`TypedPath`].
259
        ///
260
        /// See [`TypedPath`] for more details and examples.
261
        fn typed_head<H, T, P>(self, handler: H) -> Self
262
        where
263
            H: axum::handler::Handler<T, S, B>,
264
            T: SecondElementIs<P> + 'static,
265
            P: TypedPath;
266

            
267
        /// Add a typed `OPTIONS` route to the router.
268
        ///
269
        /// The path will be inferred from the first argument to the handler
270
        /// function which must implement [`TypedPath`].
271
        ///
272
        /// See [`TypedPath`] for more details and examples.
273
        fn typed_options<H, T, P>(self, handler: H) -> Self
274
        where
275
            H: axum::handler::Handler<T, S, B>,
276
            T: SecondElementIs<P> + 'static,
277
            P: TypedPath;
278

            
279
        /// Add a typed `PATCH` route to the router.
280
        ///
281
        /// The path will be inferred from the first argument to the handler
282
        /// function which must implement [`TypedPath`].
283
        ///
284
        /// See [`TypedPath`] for more details and examples.
285
        fn typed_patch<H, T, P>(self, handler: H) -> Self
286
        where
287
            H: axum::handler::Handler<T, S, B>,
288
            T: SecondElementIs<P> + 'static,
289
            P: TypedPath;
290

            
291
        /// Add a typed `POST` route to the router.
292
        ///
293
        /// The path will be inferred from the first argument to the handler
294
        /// function which must implement [`TypedPath`].
295
        ///
296
        /// See [`TypedPath`] for more details and examples.
297
        fn typed_post<H, T, P>(self, handler: H) -> Self
298
        where
299
            H: axum::handler::Handler<T, S, B>,
300
            T: SecondElementIs<P> + 'static,
301
            P: TypedPath;
302

            
303
        /// Add a typed `PUT` route to the router.
304
        ///
305
        /// The path will be inferred from the first argument to the handler
306
        /// function which must implement [`TypedPath`].
307
        ///
308
        /// See [`TypedPath`] for more details and examples.
309
        fn typed_put<H, T, P>(self, handler: H) -> Self
310
        where
311
            H: axum::handler::Handler<T, S, B>,
312
            T: SecondElementIs<P> + 'static,
313
            P: TypedPath;
314

            
315
        /// Add a typed `TRACE` route to the router.
316
        ///
317
        /// The path will be inferred from the first argument to the handler
318
        /// function which must implement [`TypedPath`].
319
        ///
320
        /// See [`TypedPath`] for more details and examples.
321
        fn typed_trace<H, T, P>(self, handler: H) -> Self
322
        where
323
            H: axum::handler::Handler<T, S, B>,
324
            T: SecondElementIs<P> + 'static,
325
            P: TypedPath;
326

            
327
        /// Add another route to the router with an additional "trailing slash
328
        /// redirect" route.
329
        ///
330
        /// If you add a route _without_ a trailing slash, such as `/foo`, this
331
        /// method will also add a route for `/foo/` that redirects to
332
        /// `/foo`.
333
        ///
334
        /// If you add a route _with_ a trailing slash, such as `/bar/`, this
335
        /// method will also add a route for `/bar` that redirects to
336
        /// `/bar/`.
337
        ///
338
        /// This is similar to what axum 0.5.x did by default, except this
339
        /// explicitly adds another route, so trying to add a `/foo/`
340
        /// route after calling `.route_with_tsr("/foo", /* ... */)`
341
        /// will result in a panic due to route overlap.
342
        ///
343
        /// # Example
344
        ///
345
        /// ```
346
        /// use axum::{routing::get, Router};
347
        /// use axum_extra::routing::RouterExt;
348
        ///
349
        /// let app = Router::new()
350
        ///     // `/foo/` will redirect to `/foo`
351
        ///     .route_with_tsr("/foo", get(|| async {}))
352
        ///     // `/bar` will redirect to `/bar/`
353
        ///     .route_with_tsr("/bar/", get(|| async {}));
354
        /// # let _: Router = app;
355
        /// ```
356
        fn route_with_tsr(self, path: &str, method_router: MethodRouter<S, B>) -> Self
357
        where
358
            Self: Sized;
359

            
360
        /// Add another route to the router with an additional "trailing slash
361
        /// redirect" route.
362
        ///
363
        /// This works like [`RouterExt::route_with_tsr`] but accepts any
364
        /// [`Service`].
365
        fn route_service_with_tsr<T>(self, path: &str, service: T) -> Self
366
        where
367
            T: Service<Request<B>, Error = Infallible> + Clone + Send + 'static,
368
            T::Response: IntoResponse,
369
            T::Future: Send + 'static,
370
            Self: Sized;
371
    }
372

            
373
    impl<S, B> RouterExt<S, B> for Router<S, B>
374
    where
375
        B: axum::body::HttpBody + Send + 'static,
376
        S: Clone + Send + Sync + 'static,
377
    {
378
32
        fn typed_get<H, T, P>(mut self, handler: H) -> Self
379
        where
380
            H: axum::handler::Handler<T, S, B>,
381
            T: SecondElementIs<P> + 'static,
382
            P: TypedPath,
383
        {
384
32
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
385
64
            self = self.route(
386
32
                tsr_path.as_ref(),
387
32
                axum::routing::get(move |url| tsr_handler_into_async(url, tsr_handler)),
388
            );
389
32
            self = self.route(P::PATH, axum::routing::get(handler));
390
32
            self
391
32
        }
392

            
393
        fn typed_delete<H, T, P>(mut self, handler: H) -> Self
394
        where
395
            H: axum::handler::Handler<T, S, B>,
396
            T: SecondElementIs<P> + 'static,
397
            P: TypedPath,
398
        {
399
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
400
            self = self.route(
401
                tsr_path.as_ref(),
402
                axum::routing::delete(move |url| tsr_handler_into_async(url, tsr_handler)),
403
            );
404
            self = self.route(P::PATH, axum::routing::delete(handler));
405
            self
406
        }
407

            
408
        fn typed_head<H, T, P>(mut self, handler: H) -> Self
409
        where
410
            H: axum::handler::Handler<T, S, B>,
411
            T: SecondElementIs<P> + 'static,
412
            P: TypedPath,
413
        {
414
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
415
            self = self.route(
416
                tsr_path.as_ref(),
417
                axum::routing::head(move |url| tsr_handler_into_async(url, tsr_handler)),
418
            );
419
            self = self.route(P::PATH, axum::routing::head(handler));
420
            self
421
        }
422

            
423
        fn typed_options<H, T, P>(mut self, handler: H) -> Self
424
        where
425
            H: axum::handler::Handler<T, S, B>,
426
            T: SecondElementIs<P> + 'static,
427
            P: TypedPath,
428
        {
429
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
430
            self = self.route(
431
                tsr_path.as_ref(),
432
                axum::routing::options(move |url| tsr_handler_into_async(url, tsr_handler)),
433
            );
434
            self = self.route(P::PATH, axum::routing::options(handler));
435
            self
436
        }
437

            
438
        fn typed_patch<H, T, P>(mut self, handler: H) -> Self
439
        where
440
            H: axum::handler::Handler<T, S, B>,
441
            T: SecondElementIs<P> + 'static,
442
            P: TypedPath,
443
        {
444
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
445
            self = self.route(
446
                tsr_path.as_ref(),
447
                axum::routing::patch(move |url| tsr_handler_into_async(url, tsr_handler)),
448
            );
449
            self = self.route(P::PATH, axum::routing::patch(handler));
450
            self
451
        }
452

            
453
32
        fn typed_post<H, T, P>(mut self, handler: H) -> Self
454
        where
455
            H: axum::handler::Handler<T, S, B>,
456
            T: SecondElementIs<P> + 'static,
457
            P: TypedPath,
458
        {
459
32
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
460
64
            self = self.route(
461
32
                tsr_path.as_ref(),
462
32
                axum::routing::post(move |url| tsr_handler_into_async(url, tsr_handler)),
463
            );
464
32
            self = self.route(P::PATH, axum::routing::post(handler));
465
32
            self
466
32
        }
467

            
468
        fn typed_put<H, T, P>(mut self, handler: H) -> Self
469
        where
470
            H: axum::handler::Handler<T, S, B>,
471
            T: SecondElementIs<P> + 'static,
472
            P: TypedPath,
473
        {
474
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
475
            self = self.route(
476
                tsr_path.as_ref(),
477
                axum::routing::put(move |url| tsr_handler_into_async(url, tsr_handler)),
478
            );
479
            self = self.route(P::PATH, axum::routing::put(handler));
480
            self
481
        }
482

            
483
        fn typed_trace<H, T, P>(mut self, handler: H) -> Self
484
        where
485
            H: axum::handler::Handler<T, S, B>,
486
            T: SecondElementIs<P> + 'static,
487
            P: TypedPath,
488
        {
489
            let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
490
            self = self.route(
491
                tsr_path.as_ref(),
492
                axum::routing::trace(move |url| tsr_handler_into_async(url, tsr_handler)),
493
            );
494
            self = self.route(P::PATH, axum::routing::trace(handler));
495
            self
496
        }
497

            
498
        #[track_caller]
499
        fn route_with_tsr(mut self, path: &str, method_router: MethodRouter<S, B>) -> Self
500
        where
501
            Self: Sized,
502
        {
503
            validate_tsr_path(path);
504
            self = self.route(path, method_router);
505
            add_tsr_redirect_route(self, path)
506
        }
507

            
508
        #[track_caller]
509
        fn route_service_with_tsr<T>(mut self, path: &str, service: T) -> Self
510
        where
511
            T: Service<Request<B>, Error = Infallible> + Clone + Send + 'static,
512
            T::Response: IntoResponse,
513
            T::Future: Send + 'static,
514
            Self: Sized,
515
        {
516
            validate_tsr_path(path);
517
            self = self.route_service(path, service);
518
            add_tsr_redirect_route(self, path)
519
        }
520
    }
521

            
522
    #[track_caller]
523
    fn validate_tsr_path(path: &str) {
524
        if path == "/" {
525
            panic!("Cannot add a trailing slash redirect route for `/`")
526
        }
527
    }
528

            
529
    #[inline]
530
    fn add_tsr_redirect_route<S, B>(router: Router<S, B>, path: &str) -> Router<S, B>
531
    where
532
        B: axum::body::HttpBody + Send + 'static,
533
        S: Clone + Send + Sync + 'static,
534
    {
535
        async fn redirect_handler(uri: Uri) -> Response {
536
            let new_uri = map_path(uri, |path| {
537
                path.strip_suffix('/')
538
                    .map(Cow::Borrowed)
539
                    .unwrap_or_else(|| Cow::Owned(format!("{path}/")))
540
            });
541

            
542
            new_uri.map_or_else(
543
                || StatusCode::BAD_REQUEST.into_response(),
544
                |new_uri| Redirect::permanent(&new_uri.to_string()).into_response(),
545
            )
546
        }
547

            
548
        if let Some(path_without_trailing_slash) = path.strip_suffix('/') {
549
            router.route(path_without_trailing_slash, any(redirect_handler))
550
        } else {
551
            router.route(&format!("{path}/"), any(redirect_handler))
552
        }
553
    }
554

            
555
    #[inline]
556
64
    fn tsr_redirect_route(path: &'_ str) -> (Cow<'_, str>, fn(Uri) -> Response) {
557
        fn redirect_handler(uri: Uri) -> Response {
558
            let new_uri = map_path(uri, |path| {
559
                path.strip_suffix('/')
560
                    .map(Cow::Borrowed)
561
                    .unwrap_or_else(|| Cow::Owned(format!("{path}/")))
562
            });
563

            
564
            new_uri.map_or_else(
565
                || StatusCode::BAD_REQUEST.into_response(),
566
                |new_uri| Redirect::permanent(&new_uri.to_string()).into_response(),
567
            )
568
        }
569

            
570
128
        path.strip_suffix('/').map_or_else(
571
64
            || {
572
                (
573
                    Cow::Owned(format!("{path}/")),
574
                    redirect_handler as fn(Uri) -> Response,
575
                )
576
            },
577
208
            |path_without_trailing_slash| {
578
208
                (
579
208
                    Cow::Borrowed(path_without_trailing_slash),
580
                    redirect_handler as fn(Uri) -> Response,
581
208
                )
582
208
            },
583
        )
584
64
    }
585

            
586
    #[inline]
587
    async fn tsr_handler_into_async(u: Uri, h: fn(Uri) -> Response) -> Response {
588
        h(u)
589
    }
590

            
591
    /// Map the path of a `Uri`.
592
    ///
593
    /// Returns `None` if the `Uri` cannot be put back together with the new
594
    /// path.
595
    fn map_path<F>(original_uri: Uri, f: F) -> Option<Uri>
596
    where
597
        F: FnOnce(&str) -> Cow<'_, str>,
598
    {
599
        let mut parts = original_uri.into_parts();
600
        let path_and_query = parts.path_and_query.as_ref()?;
601

            
602
        let new_path = f(path_and_query.path());
603

            
604
        let new_path_and_query = if let Some(query) = &path_and_query.query() {
605
            format!("{new_path}?{query}").parse::<PathAndQuery>().ok()?
606
        } else {
607
            new_path.parse::<PathAndQuery>().ok()?
608
        };
609
        parts.path_and_query = Some(new_path_and_query);
610

            
611
        Uri::from_parts(parts).ok()
612
    }
613
}