From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org [79.124.17.100]) by master.gitmailbox.com (Postfix) with ESMTP id BCADC42454 for ; Tue, 18 Oct 2022 18:23:40 +0000 (UTC) Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 6745368BD1F; Tue, 18 Oct 2022 21:23:38 +0300 (EEST) Received: from mail-ej1-f41.google.com (mail-ej1-f41.google.com [209.85.218.41]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id A9EC868BC9B for ; Tue, 18 Oct 2022 21:23:32 +0300 (EEST) Received: by mail-ej1-f41.google.com with SMTP id bj12so34301177ejb.13 for ; Tue, 18 Oct 2022 11:23:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=content-transfer-encoding:in-reply-to:from:references:to :content-language:subject:user-agent:mime-version:date:message-id :from:to:cc:subject:date:message-id:reply-to; bh=hofh49tYgEAHz5/R9rpi+3nuZOAI1TX8zqA+X+XT9/g=; b=KhTV36bj7159T7DPBkyJbUVyiRoT74TMkarKJgr3JJQlk6xvBMRcOg1evjVwZoSdsS 5gaNU97931vLqceNhMVuG+bqmr0iJD5VYTXvrBi9ad9PZ0JnGQ6vuwXUbKkkTO6eiFCO UqQ5tEuUq5I3yXGbrYqPWA7TbdH51Ehw/iFxCtNq4FyhPcqVlOzrTT5xEKlrJ6lj3ou+ HP/SCRPuaNwKHmJc1XbIIVAHDcKOD+0Pngw8Scrqhgg8qwKeK5Jnz0gVdEHmgKLHz/aa 7OP9oYLnqDGbc2wzkUI9fQOeffJP1d84r73OKDAoWBMZjHTzcGudIrlu5FHyvZo5ZwbD FMQg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=content-transfer-encoding:in-reply-to:from:references:to :content-language:subject:user-agent:mime-version:date:message-id :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=hofh49tYgEAHz5/R9rpi+3nuZOAI1TX8zqA+X+XT9/g=; b=nqaZcwScrMktM4Njw6d8gmEjjh4rQpfgVfkyio19h/e8IWHIzLbvJEy6lrlkqc9Sjw MH8IjZRafdpFdiXtMzLkLlP7/y3o6GyAmUraW+dBosGVCcfg4UeD0rSVueImEMLCJz7i RUyah+3tni5bmbmVwBAsKGJLbu/6WdhQKd4nJv98XKEb6Zzi35GMlj++yx9t+yzsh25q 8Z3ypc8Ey/uO/4oq+UGEm7bIjWJjihPQVuCrCEqTd/8lNg91z82F2cMGs90Oc0IoB9pC OKDS3O7kU9wHXRjMu4Lb63UIv9Ef6VUOFIKadQd8SF67sJhuwQfP7Dh27Rka6I5hz6he Kb/w== X-Gm-Message-State: ACrzQf3NJ26VNS/sJDSkEyHDnPfW4l2MlzlROuhue+mlcByNZ32SaGDk PmRjFrswd2t9s+DnNH0o82mOC3Qh9ik= X-Google-Smtp-Source: AMsMyM6mT3/2XkKx8mIwVYGbQ9M0JwhnKPrwQ1mSILqask1i72pl2qoEt2yl+YNDC2FO+uh/R4YBLg== X-Received: by 2002:a17:906:eecb:b0:73c:5bcb:8eb3 with SMTP id wu11-20020a170906eecb00b0073c5bcb8eb3mr3439611ejb.284.1666117411812; Tue, 18 Oct 2022 11:23:31 -0700 (PDT) Received: from [100.75.122.69] ([213.55.221.51]) by smtp.googlemail.com with ESMTPSA id k13-20020a17090627cd00b0077826b92d99sm7909502ejc.12.2022.10.18.11.23.30 for (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Tue, 18 Oct 2022 11:23:31 -0700 (PDT) Message-ID: <30896212-1aca-4cb6-c415-5f86c98dc6a3@gmail.com> Date: Tue, 18 Oct 2022 20:23:28 +0200 MIME-Version: 1.0 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Thunderbird/102.3.2 Content-Language: en-US To: ffmpeg-devel@ffmpeg.org References: From: Gregor Riepl In-Reply-To: Subject: [FFmpeg-devel] [PATCH v2] avformat/dashdec: Differentiate unassigned and zero attributes X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset="us-ascii"; Format="flowed" Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" Archived-At: List-Archive: List-Post: This fixes an issue where a timestamp attribute may have a valid zero value (the UNIX epoch 1970-01-01T00:00:00), but is misinterpreted by dashdec as being unassigned. This changes the logic that calculates segment numbers and makes the stream undecodable by dashdec. The fix originally posted to the issue tracker was incorrect and changed other parts of the segment calculation logic. With this patch, only the interpretation of the attributes is changed. Some warnings are added to account for potential errors in manifests. v2 change: Use int, 0 and 1 instead of C99 stdbool. This similar to what's done in fftools/ffmpeg_opt.c. Fixes: #8522 Signed-off-by: Gregor Riepl --- libavformat/dashdec.c | 209 ++++++++++++++++++++++++++++++++---------- 1 file changed, 161 insertions(+), 48 deletions(-) diff --git a/libavformat/dashdec.c b/libavformat/dashdec.c index 29d4680c68..df5d453c5a 100644 --- a/libavformat/dashdec.c +++ b/libavformat/dashdec.c @@ -129,21 +129,34 @@ typedef struct DASHContext { struct representation **subtitles; /* MediaPresentationDescription Attribute */ - uint64_t media_presentation_duration; - uint64_t suggested_presentation_delay; - uint64_t availability_start_time; - uint64_t availability_end_time; - uint64_t publish_time; - uint64_t minimum_update_period; - uint64_t time_shift_buffer_depth; - uint64_t min_buffer_time; + uint64_t media_presentation_duration_value; + uint64_t suggested_presentation_delay_value; + uint64_t availability_start_time_value; + uint64_t availability_end_time_value; + uint64_t publish_time_value; + uint64_t minimum_update_period_value; + uint64_t time_shift_buffer_depth_value; + uint64_t min_buffer_time_value; /* Period Attribute */ - uint64_t period_duration; - uint64_t period_start; + uint64_t period_duration_value; + uint64_t period_start_value; /* AdaptationSet Attribute */ - char *adaptionset_lang; + char *adaptionset_lang_value; + + /* Attribute valid flags (true if the attribute exists in the XML manifest) */ + int media_presentation_duration_assigned; + int suggested_presentation_delay_assigned; + int availability_start_time_assigned; + int availability_end_time_assigned; + int publish_time_assigned; + int minimum_update_period_assigned; + int time_shift_buffer_depth_assigned; + int min_buffer_time_assigned; + int period_duration_assigned; + int period_start_assigned; + int adaptionset_lang_assigned; int is_live; AVIOInterruptCB *interrupt_callback; @@ -867,8 +880,8 @@ static int parse_manifest_representation(AVFormatContext *s, const char *url, rep = av_mallocz(sizeof(struct representation)); if (!rep) return AVERROR(ENOMEM); - if (c->adaptionset_lang) { - rep->lang = av_strdup(c->adaptionset_lang); + if (c->adaptionset_lang_assigned) { + rep->lang = av_strdup(c->adaptionset_lang_value); if (!rep->lang) { av_log(s, AV_LOG_ERROR, "alloc language memory failure\n"); av_freep(&rep); @@ -1106,7 +1119,10 @@ static int parse_manifest_adaptationset_attr(AVFormatContext *s, xmlNodePtr adap av_log(s, AV_LOG_WARNING, "Cannot get AdaptionSet\n"); return AVERROR(EINVAL); } - c->adaptionset_lang = xmlGetProp(adaptionset_node, "lang"); + c->adaptionset_lang_value = xmlGetProp(adaptionset_node, "lang"); + if (c->adaptionset_lang_value) { + c->adaptionset_lang_assigned = 1; + } return 0; } @@ -1162,8 +1178,9 @@ static int parse_manifest_adaptationset(AVFormatContext *s, const char *url, } err: - xmlFree(c->adaptionset_lang); - c->adaptionset_lang = NULL; + xmlFree(c->adaptionset_lang_value); + c->adaptionset_lang_value = NULL; + c->adaptionset_lang_assigned = 0; return ret; } @@ -1273,29 +1290,37 @@ static int parse_manifest(AVFormatContext *s, const char *url, AVIOContext *in) val = xmlGetProp(node, attr->name); if (!av_strcasecmp(attr->name, "availabilityStartTime")) { - c->availability_start_time = get_utc_date_time_insec(s, val); - av_log(s, AV_LOG_TRACE, "c->availability_start_time = [%"PRId64"]\n", c->availability_start_time); + c->availability_start_time_value = get_utc_date_time_insec(s, val); + c->availability_start_time_assigned = 1; + av_log(s, AV_LOG_TRACE, "c->availability_start_time = [%"PRId64"]\n", c->availability_start_time_value); } else if (!av_strcasecmp(attr->name, "availabilityEndTime")) { - c->availability_end_time = get_utc_date_time_insec(s, val); - av_log(s, AV_LOG_TRACE, "c->availability_end_time = [%"PRId64"]\n", c->availability_end_time); + c->availability_end_time_value = get_utc_date_time_insec(s, val); + c->availability_end_time_assigned = 1; + av_log(s, AV_LOG_TRACE, "c->availability_end_time = [%"PRId64"]\n", c->availability_end_time_value); } else if (!av_strcasecmp(attr->name, "publishTime")) { - c->publish_time = get_utc_date_time_insec(s, val); - av_log(s, AV_LOG_TRACE, "c->publish_time = [%"PRId64"]\n", c->publish_time); + c->publish_time_value = get_utc_date_time_insec(s, val); + c->publish_time_assigned = 1; + av_log(s, AV_LOG_TRACE, "c->publish_time = [%"PRId64"]\n", c->publish_time_value); } else if (!av_strcasecmp(attr->name, "minimumUpdatePeriod")) { - c->minimum_update_period = get_duration_insec(s, val); - av_log(s, AV_LOG_TRACE, "c->minimum_update_period = [%"PRId64"]\n", c->minimum_update_period); + c->minimum_update_period_value = get_duration_insec(s, val); + c->minimum_update_period_assigned = 1; + av_log(s, AV_LOG_TRACE, "c->minimum_update_period = [%"PRId64"]\n", c->minimum_update_period_value); } else if (!av_strcasecmp(attr->name, "timeShiftBufferDepth")) { - c->time_shift_buffer_depth = get_duration_insec(s, val); - av_log(s, AV_LOG_TRACE, "c->time_shift_buffer_depth = [%"PRId64"]\n", c->time_shift_buffer_depth); + c->time_shift_buffer_depth_value = get_duration_insec(s, val); + c->time_shift_buffer_depth_assigned = 1; + av_log(s, AV_LOG_TRACE, "c->time_shift_buffer_depth = [%"PRId64"]\n", c->time_shift_buffer_depth_value); } else if (!av_strcasecmp(attr->name, "minBufferTime")) { - c->min_buffer_time = get_duration_insec(s, val); - av_log(s, AV_LOG_TRACE, "c->min_buffer_time = [%"PRId64"]\n", c->min_buffer_time); + c->min_buffer_time_value = get_duration_insec(s, val); + c->min_buffer_time_assigned = 1; + av_log(s, AV_LOG_TRACE, "c->min_buffer_time = [%"PRId64"]\n", c->min_buffer_time_value); } else if (!av_strcasecmp(attr->name, "suggestedPresentationDelay")) { - c->suggested_presentation_delay = get_duration_insec(s, val); - av_log(s, AV_LOG_TRACE, "c->suggested_presentation_delay = [%"PRId64"]\n", c->suggested_presentation_delay); + c->suggested_presentation_delay_value = get_duration_insec(s, val); + c->suggested_presentation_delay_assigned = 1; + av_log(s, AV_LOG_TRACE, "c->suggested_presentation_delay = [%"PRId64"]\n", c->suggested_presentation_delay_value); } else if (!av_strcasecmp(attr->name, "mediaPresentationDuration")) { - c->media_presentation_duration = get_duration_insec(s, val); - av_log(s, AV_LOG_TRACE, "c->media_presentation_duration = [%"PRId64"]\n", c->media_presentation_duration); + c->media_presentation_duration_value = get_duration_insec(s, val); + c->media_presentation_duration_assigned = 1; + av_log(s, AV_LOG_TRACE, "c->media_presentation_duration = [%"PRId64"]\n", c->media_presentation_duration_value); } attr = attr->next; xmlFree(val); @@ -1325,12 +1350,30 @@ static int parse_manifest(AVFormatContext *s, const char *url, AVIOContext *in) attr = attr->next; xmlFree(val); } - if ((period_duration_sec) >= (c->period_duration)) { + if (c->period_duration_assigned) { + if ((period_duration_sec) >= (c->period_duration_value)) { + period_node = node; + c->period_duration_value = period_duration_sec; + c->period_start_value = period_start_sec; + c->period_start_assigned = 1; + if (c->period_start_value > 0) { + c->media_presentation_duration_value = c->period_duration_value; + c->media_presentation_duration_assigned = 1; + } + } else { + av_log(s, AV_LOG_VERBOSE, "previous period_duration is larger than new value. ignoring.\n"); + } + } else { + av_log(s, AV_LOG_VERBOSE, "period_duration attribute unset - updating from calculated value.\n"); period_node = node; - c->period_duration = period_duration_sec; - c->period_start = period_start_sec; - if (c->period_start > 0) - c->media_presentation_duration = c->period_duration; + c->period_duration_value = period_duration_sec; + c->period_duration_assigned = 1; + c->period_start_value = period_start_sec; + c->period_start_assigned = 1; + if (c->period_start_value > 0) { + c->media_presentation_duration_value = c->period_duration_value; + c->media_presentation_duration_assigned = 1; + } } } else if (!av_strcasecmp(node->name, "ProgramInformation")) { parse_programinformation(s, node); @@ -1391,15 +1434,54 @@ static int64_t calc_cur_seg_no(AVFormatContext *s, struct representation *pls) } else if (pls->fragment_duration){ av_log(s, AV_LOG_TRACE, "in fragment_duration mode fragment_timescale = %"PRId64", presentation_timeoffset = %"PRId64"\n", pls->fragment_timescale, pls->presentation_timeoffset); if (pls->presentation_timeoffset) { - num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time) * pls->fragment_timescale)-pls->presentation_timeoffset) / pls->fragment_duration - c->min_buffer_time; - } else if (c->publish_time > 0 && !c->availability_start_time) { - if (c->min_buffer_time) { - num = pls->first_seq_no + (((c->publish_time + pls->fragment_duration) - c->suggested_presentation_delay) * pls->fragment_timescale) / pls->fragment_duration - c->min_buffer_time; + if (c->availability_start_time_assigned && c->min_buffer_time_assigned) { + num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time_value) * pls->fragment_timescale)-pls->presentation_timeoffset) / pls->fragment_duration - c->min_buffer_time_assigned; } else { - num = pls->first_seq_no + (((c->publish_time - c->time_shift_buffer_depth + pls->fragment_duration) - c->suggested_presentation_delay) * pls->fragment_timescale) / pls->fragment_duration; + av_log(s, AV_LOG_WARNING, "availability_start_time and/or min_buffer_time attributes unset - using zero values. segment numbers may be incorrect.\n"); + if (c->availability_start_time_assigned) { + num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time_value) * pls->fragment_timescale)-pls->presentation_timeoffset) / pls->fragment_duration; + } else if (c->min_buffer_time_assigned) { + num = pls->first_seq_no + (((get_current_time_in_sec()) * pls->fragment_timescale)-pls->presentation_timeoffset) / pls->fragment_duration - c->min_buffer_time_value; + } else { + num = pls->first_seq_no + (((get_current_time_in_sec()) * pls->fragment_timescale)-pls->presentation_timeoffset) / pls->fragment_duration; + } + } + } else if (c->publish_time_assigned && c->publish_time_value > 0 && !c->availability_start_time_assigned) { + // FIXME is publish_time_value > 0 a required condition, or are we only checking for existence of the attribute? + if (c->min_buffer_time_assigned) { + if (c->suggested_presentation_delay_assigned) { + num = pls->first_seq_no + (((c->publish_time_value + pls->fragment_duration) - c->suggested_presentation_delay_value) * pls->fragment_timescale) / pls->fragment_duration - c->min_buffer_time_value; + } else { + av_log(s, AV_LOG_WARNING, "suggested_presentation_delay attribute unset - using zero value. segment numbers may be incorrect.\n"); + num = pls->first_seq_no + ((c->publish_time_value + pls->fragment_duration) * pls->fragment_timescale) / pls->fragment_duration - c->min_buffer_time_value; + } + } else { + if (c->time_shift_buffer_depth_assigned && c->suggested_presentation_delay_assigned) { + num = pls->first_seq_no + (((c->publish_time_value - c->time_shift_buffer_depth_value + pls->fragment_duration) - c->suggested_presentation_delay_value) * pls->fragment_timescale) / pls->fragment_duration; + } else { + av_log(s, AV_LOG_WARNING, "time_shift_buffer_depth and/or suggested_presentation_delay attributes unset - using zero values. segment numbers may be incorrect.\n"); + if (c->time_shift_buffer_depth_assigned) { + num = pls->first_seq_no + ((c->publish_time_value - c->time_shift_buffer_depth_value + pls->fragment_duration) * pls->fragment_timescale) / pls->fragment_duration; + } else if (c->suggested_presentation_delay_assigned) { + num = pls->first_seq_no + (((c->publish_time_value + pls->fragment_duration) - c->suggested_presentation_delay_value) * pls->fragment_timescale) / pls->fragment_duration; + } else { + num = pls->first_seq_no + ((c->publish_time_value + pls->fragment_duration) * pls->fragment_timescale) / pls->fragment_duration; + } + } } } else { - num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time) - c->suggested_presentation_delay) * pls->fragment_timescale) / pls->fragment_duration; + if (c->availability_start_time_assigned && c->suggested_presentation_delay_assigned) { + num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time_value) - c->suggested_presentation_delay_value) * pls->fragment_timescale) / pls->fragment_duration; + } else { + av_log(s, AV_LOG_WARNING, "availability_start_time and/or suggested_presentation_delay attributes unset - using zero values. segment numbers may be incorrect.\n"); + if (c->availability_start_time_assigned) { + num = pls->first_seq_no + ((get_current_time_in_sec() - c->availability_start_time_value) * pls->fragment_timescale) / pls->fragment_duration; + } else if (c->suggested_presentation_delay_assigned) { + num = pls->first_seq_no + ((get_current_time_in_sec() - c->suggested_presentation_delay_value) * pls->fragment_timescale) / pls->fragment_duration; + } else { + num = pls->first_seq_no + (get_current_time_in_sec() * pls->fragment_timescale) / pls->fragment_duration; + } + } } } } else { @@ -1415,7 +1497,18 @@ static int64_t calc_min_seg_no(AVFormatContext *s, struct representation *pls) if (c->is_live && pls->fragment_duration) { av_log(s, AV_LOG_TRACE, "in live mode\n"); - num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time) - c->time_shift_buffer_depth) * pls->fragment_timescale) / pls->fragment_duration; + if (c->availability_start_time_assigned && c->time_shift_buffer_depth_assigned) { + num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time_value) - c->time_shift_buffer_depth_value) * pls->fragment_timescale) / pls->fragment_duration; + } else { + av_log(s, AV_LOG_WARNING, "availability_start_time and/or time_shift_buffer_depth attributes unset - using zero values. segment numbers may be incorrect.\n"); + if (c->availability_start_time_assigned) { + num = pls->first_seq_no + ((get_current_time_in_sec() - c->availability_start_time_value) * pls->fragment_timescale) / pls->fragment_duration; + } else if (c->time_shift_buffer_depth_assigned) { + num = pls->first_seq_no + ((get_current_time_in_sec() - c->time_shift_buffer_depth_value) * pls->fragment_timescale) / pls->fragment_duration; + } else { + num = pls->first_seq_no + (get_current_time_in_sec() * pls->fragment_timescale) / pls->fragment_duration; + } + } } else { num = pls->first_seq_no; } @@ -1434,15 +1527,30 @@ static int64_t calc_max_seg_no(struct representation *pls, DASHContext *c) for (i = 0; i < pls->n_timelines; i++) { if (pls->timelines[i]->repeat == -1) { int length_of_each_segment = pls->timelines[i]->duration / pls->fragment_timescale; - num = c->period_duration / length_of_each_segment; + if (c->period_duration_assigned) { + num = c->period_duration_value / length_of_each_segment; + } else { + av_log(NULL, AV_LOG_WARNING, "period_duration attribute unset - using zero value. segment numbers may be incorrect.\n"); + num = 0; + } } else { num += pls->timelines[i]->repeat; } } } else if (c->is_live && pls->fragment_duration) { - num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time)) * pls->fragment_timescale) / pls->fragment_duration; + if (c->availability_start_time_assigned) { + num = pls->first_seq_no + (((get_current_time_in_sec() - c->availability_start_time_value)) * pls->fragment_timescale) / pls->fragment_duration; + } else { + av_log(NULL, AV_LOG_WARNING, "availability_start_time attribute unset - using zero value. segment numbers may be incorrect.\n"); + num = pls->first_seq_no + (get_current_time_in_sec() * pls->fragment_timescale) / pls->fragment_duration; + } } else if (pls->fragment_duration) { - num = pls->first_seq_no + av_rescale_rnd(1, c->media_presentation_duration * pls->fragment_timescale, pls->fragment_duration, AV_ROUND_UP); + if (c->media_presentation_duration_assigned) { + num = pls->first_seq_no + av_rescale_rnd(1, c->media_presentation_duration_value * pls->fragment_timescale, pls->fragment_duration, AV_ROUND_UP); + } else { + av_log(NULL, AV_LOG_WARNING, "media_presentation_duration attribute unset - using zero value. segment numbers may be incorrect.\n"); + num = pls->first_seq_no + av_rescale_rnd(1, 0, pls->fragment_duration, AV_ROUND_UP); + } } return num; @@ -2040,7 +2148,12 @@ static int dash_read_header(AVFormatContext *s) /* If this isn't a live stream, fill the total duration of the * stream. */ if (!c->is_live) { - s->duration = (int64_t) c->media_presentation_duration * AV_TIME_BASE; + if (c->media_presentation_duration_assigned) { + s->duration = (int64_t) c->media_presentation_duration_value * AV_TIME_BASE; + } else { + av_log(NULL, AV_LOG_WARNING, "media_presentation_duration attribute unset - using zero value. segment numbers may be incorrect.\n"); + s->duration = 0; + } } else { av_dict_set(&c->avio_opts, "seekable", "0", 0); } -- 2.35.1 _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".